Core Data性能优化及多上下文操作
1 前言
尽管CoreData内部已经做了大量优化,但是由于不合理的模型和效率低下的自定义查找和抓取逻辑仍然可能会降低CoreData的效率。CoreData的性能是在内存占用率和速度之间平衡的结果,一个好的APP需要使用尽可能的少的内存占有率达到相对快的效率。
Measure, Change, Verify
对于每一次性能调优,应当遵循Measure:测试当前性能, change:做出适当修改, verify:验证是否达到优化目的。直至最终达到要求的性能。
2 性能优化
CoreData优化主要是从设计合理的模型NSManagedObjectModle,设计合理的查询逻辑两个方向出发:
2.1 合理的模型NSManagedObjectModle
在优化模型时,通常可以使用XCode自带的内存管理工具来查看程序内存占用,优化程序性能。
当CoreData访问内存中一调记录时,会将这条记录的所有属性和关系的地址加载到内存中。这意味着尽管只访问了一条记录Record的name属性时,CoreData会将Record的所有属性加载到内存中。这个操作需要从两个角度考量,一方面系统需要时间取磁盘上读取这些数据,另一方面系统需要内存空间用于存储这些数据。因此如果Record对象中存在大容量数据如NSData类型的Image时,读取大量的Record对象时会耗费大量时间,也会占用大量内存,显然这不是合理的模型。
此时这些属性应单独抽像成一个实体Attachment,通常在Record中只保存一个Attachment的关系,这样Image只有在访问Attachment时才会被加载到内存中,从而降低内存的使用率和提高程序执行速度。
2.2 合理的查询逻辑
在优化查询逻辑时,可以通过XCode自带的Instrument工具中的CoreData模板来分析优化查询效率。注意改方法只能使用模拟器,因为真机中不包含该方法所必须的DTrace工具。(目前在macOS 10.12,Xcode 8.3无法抓取任何数据,App Developer论坛上也未发现解决方案,可能是一个系统Bug)。另外通过XCTest也可以测试CoreData的性能,通过这种方式能判断出某个查询逻辑执行所需要的时间。其使用方法如下。
func testTotalEmployeesPerDepartment() {
measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: false) {
let departmentList = DepartmentListViewController()
departmentList.coreDataStack = CoreDataStack(modelName: "EmployeeDirectory")
self.startMeasuring()
_ = departmentList.totalEmployeesPerDepartment()
self.stopMeasuring()
}
}
为了优化程序性能,必须很好的平衡每次查询记录的数量和内存的消耗率。高效率的查询逻辑不会查询冗余的数据。
2.2.1 分批抓取
在以Tableview展示所有Person对象的案例中,初次进入Tableview视图并不需要将数据库中所有的Person对象都查询出来。这类情况可以通过CoreData的fetchBatchSize进行优化,通过设置batchSize,CoreData每次只查询指定数量的记录,当需要更多数据的时候,CoreData会自动执行新的批次查询操作。fetchBatchSize通常指定为当前页面需要展示的记录量的两倍。
2.2.2 谓词NSPredicate
当使用复合谓词时,尽量将更容易给数据分类的条件放在前面。例如使用如下格式的谓词“(active == YES) AND (name CONTAINS[cd] %@)”会比使用后面这个谓词"(name CONTAINS[cd] %@) AND (active == YES)"更高效。更多的谓词使用方法见官网文档。
2.2.3 查询类型FetchType和表达式NSExpression
dictionaryResultType
将fetchRequest的resultType设置为dictionaryResultType,并配置合适的NSExpression对数据进行统计可以极大的提示查询效率。NSExpression可以提供多种函数的统计工作,如count、sum、min等函数,具体用法见官方文档,这里只演示count方法。
func totalEmployeesPerDepartmentFast() -> [[String: String]] {
//1 创建NSExpressionDescription命名为“headCount”
let expressionDescreption = NSExpressionDescription()
expressionDescreption.name = "headCount"
//2 创建函数统计每个"department"的成员数量,更多的函数关键字如average,sum,count,min等见NSExpression文档
expressionDescreption.expression =
NSExpression(forFunction: "count:",
arguments: [NSExpression(forKeyPath: "department")])
//3 通过设置propertiesToFetch初始化fetch的内容,这样CoreData就不会查寻每条记录的所有数据,这里只查询"department"属性,并通过expressionDescreption函数记录不同"department"的数量。
let fetchRequest: NSFetchRequest<NSDictionary> = NSFetchRequest(entityName: "Employee")
// 这两个参数都是必须的,第一个"department"只会关注对应的属性并不会关注统计,其对应结果是【"department":name】的字典,第二个参数expressionDescreption只关注统计结果并不关注具体是哪一个department,其结果是【"headCount":value】的字典
fetchRequest.propertiesToFetch = ["department", expressionDescreption]
//查询结果以"department"分组,这样将返回一个数组
fetchRequest.propertiesToGroupBy = ["department"]
fetchRequest.resultType = .dictionaryResultType
//4 执行查询操作
var fetchResults: [NSDictionary] = []
do {
fetchResults = try coreDataStack.mainContext.fetch(fetchRequest)
} catch let error as NSError {
print("ERROR: \(error.localizedDescription)")
return [[String: String]]()
}
//5 查询的结果是一个[NSDictionary],其中元素个数取决于fetchRequest.propertiesToGroupBy的分组个数,每个字典的元素个数取决于fetchRequest.propertiesToFetch中的个数。在上述两个属性都未设置时,其结果为[NSManagedObject]。
return fetchResults as! [[String: String]]
}
执行Context的count方法
在查找一个实体的数量,并且并不关心其具体属性时可以使用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"
}
}
小结
在优化查询逻辑的时候,当实体Employee有一对多的关系Sales时,当只需要知道某个Employee有多少个Sale时除了上述两种方法查询数量,还可以直接通过Employee的关系Salse集合数量直接获取。
func salesCountForEmployeeSimple(_ employee: Employee) -> String {
return "\(employee.sales.count)"
}
该方法代码结构较前两个方法更简单,易于理解。在效率方面,经过XCTest,这种方式耗时介于上述两个方法之间,因为当访问关系时尽管不会像第一个方法中那样查询所有的Sales对象,但是仍会访问该Employee的所有Sales对象,并将它们加载到内存中。
3 多上下文操作(Multiple Managed Object Contexts)
在CoreData中,通常使用和主线程关联的Context(使用CoreDataStack初始化时即是Container中的viewContext)执行存储和修改操作。如果直接使用GCD的方式进行上述的多线程操作是线程不安全的,需要利用CoreData提供的多上下文操作(Multiple Managed Object Contexts),CoreData内部负责处理线程安全问题。
对于一个APP,尽管大多数任务在主线程使用一个ManagedContext即可,当导入的文件过大需要大量时间时,或者当希望对一些实例做一些临时的编辑并且并不希望将其存到数据库中的时候,仍需要使用多个ManagedContext。在CoreData中,每个ManagedContext的.concurrencyType属性有三中类型:
- ConfinementConcurrencyType:这种类开发者需要手动管理线程的转换,这类通常不用。
- PrivateConcurrencyType:这类Context将会和一个私有的分发队列相关联,其中的任务不会阻塞主线程,Container中调用performBackgroundTask或者newBackgroundContext得到的都是这类Context。
- MainQueueConcurrencyType:这类Context和主队列关联,其中的任务会阻塞主线程,和PersistenceStore直接关联的mainContext就属于这类,Container中调用viewContext得到的也是这类context。通常CoreData中绝大部分操作也是由这类Context完成。
当NSManagedObjectContext创建时指定了其关联的队列时,它提供了两个对象方法perform和performAndWait分别用于同步和异步执行任务。调用上述两个方法时CoreData会讲block中的代码切换到context关联的线程中执行。这里需要注意的是,performAndWait可以嵌套使用,不会出现线程死锁。
3.1 耗时任务处理
当需要执行某个耗时任务时可以使用后台上下文执行任务,可以通过新建一个类型为PrivateConcurrencyType的上下文Context并将其和当前的Coordinator关联。但是通常直接调用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 临时编辑任务
首先,需要了解CoreData中NSManagedContext和NSPersistentStore之间的关系。NSPersistentStore可以和多个Context关联,但是通常NSPersistentStore和一个主mainContext关联,当mainContext中执行编辑操作后,执行提交存储后会直接改变数据库,但是当mainContext的子上下文childContext执行提交存储后,其改动只会被提交到mainContext中,只有当mainContext下次提交时,数据库才会做改动。
当执行了某些编辑操作,并希望将这些操作单独保存在一个临时的区域,在后面某个时刻决定提交还是丢弃。此时就可以通过为mainContext创建一个子上下文childContext来完成相关操作。这里需要注意在CoreData中对一个实例对象的创建、编辑和删除操作必须位于同一个上下文中。在多上下文操作实例对象时,正确的方法是新建一个上下文childContext,将其parent属性设置为mainContext,再通过mainContext创建需要编辑的实体objectMain,此时CoreData会自动在childContext中关联一个新的实体objectChild,可以通过objectMain的objectid得到。最后使用childContext和objectChild进行相关编辑即可。
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
//这里需要注意的是实体object对于context是weak弱引用,因此都需要传递给下一个接收者
detailViewController.journalEntry = childEntry
detailViewController.context = childContext
detailViewController.delegate = self
上面代码展示了修改已有实体需先从mainContext中取出,再由objectid获得,但是当新建实体时可以直接使用childContext创建,CoreData同样也会在mainContext中创建对应的实体,并且当childContext执行save方法后,这些改变会被提交到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