1 前言
盡管CoreData內(nèi)部已經(jīng)做了大量?jī)?yōu)化,但是由于不合理的模型和效率低下的自定義查找和抓取邏輯仍然可能會(huì)降低CoreData的效率。CoreData的性能是在內(nèi)存占用率和速度之間平衡的結(jié)果,一個(gè)好的APP需要使用盡可能的少的內(nèi)存占有率達(dá)到相對(duì)快的效率。
Measure, Change, Verify
對(duì)于每一次性能調(diào)優(yōu),應(yīng)當(dāng)遵循Measure:測(cè)試當(dāng)前性能, change:做出適當(dāng)修改, verify:驗(yàn)證是否達(dá)到優(yōu)化目的。直至最終達(dá)到要求的性能。
2 性能優(yōu)化
CoreData優(yōu)化主要是從設(shè)計(jì)合理的模型NSManagedObjectModle,設(shè)計(jì)合理的查詢邏輯兩個(gè)方向出發(fā):
2.1 合理的模型NSManagedObjectModle
在優(yōu)化模型時(shí),通??梢允褂肵Code自帶的內(nèi)存管理工具來(lái)查看程序內(nèi)存占用,優(yōu)化程序性能。
當(dāng)CoreData訪問(wèn)內(nèi)存中一調(diào)記錄時(shí),會(huì)將這條記錄的所有屬性和關(guān)系的地址加載到內(nèi)存中。這意味著盡管只訪問(wèn)了一條記錄Record的name屬性時(shí),CoreData會(huì)將Record的所有屬性加載到內(nèi)存中。這個(gè)操作需要從兩個(gè)角度考量,一方面系統(tǒng)需要時(shí)間取磁盤上讀取這些數(shù)據(jù),另一方面系統(tǒng)需要內(nèi)存空間用于存儲(chǔ)這些數(shù)據(jù)。因此如果Record對(duì)象中存在大容量數(shù)據(jù)如NSData類型的Image時(shí),讀取大量的Record對(duì)象時(shí)會(huì)耗費(fèi)大量時(shí)間,也會(huì)占用大量?jī)?nèi)存,顯然這不是合理的模型。
此時(shí)這些屬性應(yīng)單獨(dú)抽像成一個(gè)實(shí)體Attachment,通常在Record中只保存一個(gè)Attachment的關(guān)系,這樣Image只有在訪問(wèn)Attachment時(shí)才會(huì)被加載到內(nèi)存中,從而降低內(nèi)存的使用率和提高程序執(zhí)行速度。
2.2 合理的查詢邏輯
在優(yōu)化查詢邏輯時(shí),可以通過(guò)XCode自帶的Instrument工具中的CoreData模板來(lái)分析優(yōu)化查詢效率。注意改方法只能使用模擬器,因?yàn)檎鏅C(jī)中不包含該方法所必須的DTrace工具。(目前在macOS 10.12,Xcode 8.3無(wú)法抓取任何數(shù)據(jù),App Developer論壇上也未發(fā)現(xiàn)解決方案,可能是一個(gè)系統(tǒng)Bug)。另外通過(guò)XCTest也可以測(cè)試CoreData的性能,通過(guò)這種方式能判斷出某個(gè)查詢邏輯執(zhí)行所需要的時(shí)間。其使用方法如下。
func testTotalEmployeesPerDepartment() {
measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: false) {
let departmentList = DepartmentListViewController()
departmentList.coreDataStack = CoreDataStack(modelName: "EmployeeDirectory")
self.startMeasuring()
_ = departmentList.totalEmployeesPerDepartment()
self.stopMeasuring()
}
}
為了優(yōu)化程序性能,必須很好的平衡每次查詢記錄的數(shù)量和內(nèi)存的消耗率。高效率的查詢邏輯不會(huì)查詢?nèi)哂嗟臄?shù)據(jù)。
2.2.1 分批抓取
在以Tableview展示所有Person對(duì)象的案例中,初次進(jìn)入Tableview視圖并不需要將數(shù)據(jù)庫(kù)中所有的Person對(duì)象都查詢出來(lái)。這類情況可以通過(guò)CoreData的fetchBatchSize進(jìn)行優(yōu)化,通過(guò)設(shè)置batchSize,CoreData每次只查詢指定數(shù)量的記錄,當(dāng)需要更多數(shù)據(jù)的時(shí)候,CoreData會(huì)自動(dòng)執(zhí)行新的批次查詢操作。fetchBatchSize通常指定為當(dāng)前頁(yè)面需要展示的記錄量的兩倍。
2.2.2 謂詞NSPredicate
當(dāng)使用復(fù)合謂詞時(shí),盡量將更容易給數(shù)據(jù)分類的條件放在前面。例如使用如下格式的謂詞“(active == YES) AND (name CONTAINS[cd] %@)”會(huì)比使用后面這個(gè)謂詞"(name CONTAINS[cd] %@) AND (active == YES)"更高效。更多的謂詞使用方法見(jiàn)官網(wǎng)文檔。
2.2.3 查詢類型FetchType和表達(dá)式NSExpression
dictionaryResultType
將fetchRequest的resultType設(shè)置為dictionaryResultType,并配置合適的NSExpression對(duì)數(shù)據(jù)進(jìn)行統(tǒng)計(jì)可以極大的提示查詢效率。NSExpression可以提供多種函數(shù)的統(tǒng)計(jì)工作,如count、sum、min等函數(shù),具體用法見(jiàn)官方文檔,這里只演示count方法。
func totalEmployeesPerDepartmentFast() -> [[String: String]] {
//1 創(chuàng)建NSExpressionDescription命名為“headCount”
let expressionDescreption = NSExpressionDescription()
expressionDescreption.name = "headCount"
//2 創(chuàng)建函數(shù)統(tǒng)計(jì)每個(gè)"department"的成員數(shù)量,更多的函數(shù)關(guān)鍵字如average,sum,count,min等見(jiàn)NSExpression文檔
expressionDescreption.expression =
NSExpression(forFunction: "count:",
arguments: [NSExpression(forKeyPath: "department")])
//3 通過(guò)設(shè)置propertiesToFetch初始化fetch的內(nèi)容,這樣CoreData就不會(huì)查尋每條記錄的所有數(shù)據(jù),這里只查詢"department"屬性,并通過(guò)expressionDescreption函數(shù)記錄不同"department"的數(shù)量。
let fetchRequest: NSFetchRequest<NSDictionary> = NSFetchRequest(entityName: "Employee")
// 這兩個(gè)參數(shù)都是必須的,第一個(gè)"department"只會(huì)關(guān)注對(duì)應(yīng)的屬性并不會(huì)關(guān)注統(tǒng)計(jì),其對(duì)應(yīng)結(jié)果是【"department":name】的字典,第二個(gè)參數(shù)expressionDescreption只關(guān)注統(tǒng)計(jì)結(jié)果并不關(guān)注具體是哪一個(gè)department,其結(jié)果是【"headCount":value】的字典
fetchRequest.propertiesToFetch = ["department", expressionDescreption]
//查詢結(jié)果以"department"分組,這樣將返回一個(gè)數(shù)組
fetchRequest.propertiesToGroupBy = ["department"]
fetchRequest.resultType = .dictionaryResultType
//4 執(zhí)行查詢操作
var fetchResults: [NSDictionary] = []
do {
fetchResults = try coreDataStack.mainContext.fetch(fetchRequest)
} catch let error as NSError {
print("ERROR: \(error.localizedDescription)")
return [[String: String]]()
}
//5 查詢的結(jié)果是一個(gè)[NSDictionary],其中元素個(gè)數(shù)取決于fetchRequest.propertiesToGroupBy的分組個(gè)數(shù),每個(gè)字典的元素個(gè)數(shù)取決于fetchRequest.propertiesToFetch中的個(gè)數(shù)。在上述兩個(gè)屬性都未設(shè)置時(shí),其結(jié)果為[NSManagedObject]。
return fetchResults as! [[String: String]]
}
執(zhí)行Context的count方法
在查找一個(gè)實(shí)體的數(shù)量,并且并不關(guān)心其具體屬性時(shí)可以使用NSManagedContext的count方法。
func salesCountForEmployeeFast(_ employee: Employee) -> String {
let fetchRequest: NSFetchRequest<Sale> = NSFetchRequest(entityName: "Sale")
let predicate = NSPredicate(format: "employee == %@", employee)
fetchRequest.predicate = predicate
let context = employee.managedObjectContext!
do {
let results = try context.count(for: fetchRequest)
return "\(results)"
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
return "0"
}
}
小結(jié)
在優(yōu)化查詢邏輯的時(shí)候,當(dāng)實(shí)體Employee有一對(duì)多的關(guān)系Sales時(shí),當(dāng)只需要知道某個(gè)Employee有多少個(gè)Sale時(shí)除了上述兩種方法查詢數(shù)量,還可以直接通過(guò)Employee的關(guān)系Salse集合數(shù)量直接獲取。
func salesCountForEmployeeSimple(_ employee: Employee) -> String {
return "\(employee.sales.count)"
}
該方法代碼結(jié)構(gòu)較前兩個(gè)方法更簡(jiǎn)單,易于理解。在效率方面,經(jīng)過(guò)XCTest,這種方式耗時(shí)介于上述兩個(gè)方法之間,因?yàn)楫?dāng)訪問(wèn)關(guān)系時(shí)盡管不會(huì)像第一個(gè)方法中那樣查詢所有的Sales對(duì)象,但是仍會(huì)訪問(wèn)該Employee的所有Sales對(duì)象,并將它們加載到內(nèi)存中。
3 多上下文操作(Multiple Managed Object Contexts)
在CoreData中,通常使用和主線程關(guān)聯(lián)的Context(使用CoreDataStack初始化時(shí)即是Container中的viewContext)執(zhí)行存儲(chǔ)和修改操作。如果直接使用GCD的方式進(jìn)行上述的多線程操作是線程不安全的,需要利用CoreData提供的多上下文操作(Multiple Managed Object Contexts),CoreData內(nèi)部負(fù)責(zé)處理線程安全問(wèn)題。
對(duì)于一個(gè)APP,盡管大多數(shù)任務(wù)在主線程使用一個(gè)ManagedContext即可,當(dāng)導(dǎo)入的文件過(guò)大需要大量時(shí)間時(shí),或者當(dāng)希望對(duì)一些實(shí)例做一些臨時(shí)的編輯并且并不希望將其存到數(shù)據(jù)庫(kù)中的時(shí)候,仍需要使用多個(gè)ManagedContext。在CoreData中,每個(gè)ManagedContext的.concurrencyType屬性有三中類型:
- ConfinementConcurrencyType:這種類開(kāi)發(fā)者需要手動(dòng)管理線程的轉(zhuǎn)換,這類通常不用。
- PrivateConcurrencyType:這類Context將會(huì)和一個(gè)私有的分發(fā)隊(duì)列相關(guān)聯(lián),其中的任務(wù)不會(huì)阻塞主線程,Container中調(diào)用performBackgroundTask或者newBackgroundContext得到的都是這類Context。
- MainQueueConcurrencyType:這類Context和主隊(duì)列關(guān)聯(lián),其中的任務(wù)會(huì)阻塞主線程,和PersistenceStore直接關(guān)聯(lián)的mainContext就屬于這類,Container中調(diào)用viewContext得到的也是這類context。通常CoreData中絕大部分操作也是由這類Context完成。
當(dāng)NSManagedObjectContext創(chuàng)建時(shí)指定了其關(guān)聯(lián)的隊(duì)列時(shí),它提供了兩個(gè)對(duì)象方法perform和performAndWait分別用于同步和異步執(zhí)行任務(wù)。調(diào)用上述兩個(gè)方法時(shí)CoreData會(huì)講block中的代碼切換到context關(guān)聯(lián)的線程中執(zhí)行。這里需要注意的是,performAndWait可以嵌套使用,不會(huì)出現(xiàn)線程死鎖。
3.1 耗時(shí)任務(wù)處理
當(dāng)需要執(zhí)行某個(gè)耗時(shí)任務(wù)時(shí)可以使用后臺(tái)上下文執(zhí)行任務(wù),可以通過(guò)新建一個(gè)類型為PrivateConcurrencyType的上下文Context并將其和當(dāng)前的Coordinator關(guān)聯(lián)。但是通常直接調(diào)用Container的performBackgroundTask方法。
coreDataStack.storeContainer.performBackgroundTask { (context) in
var results: [JournalEntry] = []
do {
results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
}
3.2 臨時(shí)編輯任務(wù)
首先,需要了解CoreData中NSManagedContext和NSPersistentStore之間的關(guān)系。NSPersistentStore可以和多個(gè)Context關(guān)聯(lián),但是通常NSPersistentStore和一個(gè)主mainContext關(guān)聯(lián),當(dāng)mainContext中執(zhí)行編輯操作后,執(zhí)行提交存儲(chǔ)后會(huì)直接改變數(shù)據(jù)庫(kù),但是當(dāng)mainContext的子上下文childContext執(zhí)行提交存儲(chǔ)后,其改動(dòng)只會(huì)被提交到mainContext中,只有當(dāng)mainContext下次提交時(shí),數(shù)據(jù)庫(kù)才會(huì)做改動(dòng)。
當(dāng)執(zhí)行了某些編輯操作,并希望將這些操作單獨(dú)保存在一個(gè)臨時(shí)的區(qū)域,在后面某個(gè)時(shí)刻決定提交還是丟棄。此時(shí)就可以通過(guò)為mainContext創(chuàng)建一個(gè)子上下文childContext來(lái)完成相關(guān)操作。這里需要注意在CoreData中對(duì)一個(gè)實(shí)例對(duì)象的創(chuàng)建、編輯和刪除操作必須位于同一個(gè)上下文中。在多上下文操作實(shí)例對(duì)象時(shí),正確的方法是新建一個(gè)上下文childContext,將其parent屬性設(shè)置為mainContext,再通過(guò)mainContext創(chuàng)建需要編輯的實(shí)體objectMain,此時(shí)CoreData會(huì)自動(dòng)在childContext中關(guān)聯(lián)一個(gè)新的實(shí)體objectChild,可以通過(guò)objectMain的objectid得到。最后使用childContext和objectChild進(jìn)行相關(guān)編輯即可。
guard let navigationController = segue.destination as? UINavigationController,
let detailViewController = navigationController.topViewController as? JournalEntryViewController,
let indexPath = tableView.indexPathForSelectedRow else {
fatalError("Application storyboard mis-configuration")
}
let surfJournalEntry = fetchedResultsController.object(at: indexPath)
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
childContext.parent = coreDataStack.mainContext
let childEntry = childContext.object(with: surfJournalEntry.objectID) as? JournalEntry
//這里需要注意的是實(shí)體object對(duì)于context是weak弱引用,因此都需要傳遞給下一個(gè)接收者
detailViewController.journalEntry = childEntry
detailViewController.context = childContext
detailViewController.delegate = self
上面代碼展示了修改已有實(shí)體需先從mainContext中取出,再由objectid獲得,但是當(dāng)新建實(shí)體時(shí)可以直接使用childContext創(chuàng)建,CoreData同樣也會(huì)在mainContext中創(chuàng)建對(duì)應(yīng)的實(shí)體,并且當(dāng)childContext執(zhí)行save方法后,這些改變會(huì)被提交到mainContext中。
guard let navigationController = segue.destination as? UINavigationController,
let detailViewController = navigationController.topViewController as? JournalEntryViewController else {
fatalError("Application storyboard mis-configuration")
}
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
childContext.parent = coreDataStack.mainContext
let newJournalEntry = JournalEntry(context: childContext)
detailViewController.journalEntry = newJournalEntry
detailViewController.context = newJournalEntry.managedObjectContext
detailViewController.delegate = self