走进Core Data的世界

Core Data作为一个OS X和iOS中自带的数据存储框架,很早就存在了。因其存在一些缺点使得很多人放弃使用而采用其它方案。但苹果依然在每个iOS版本中不断对其改进,例如在iOS 8中加入了BetchUpdate,解决了之前一直令人诟病的批量更新问题,使得Core Data更为强大。

0. Core Data Stack

先说说Core Data Stack吧,主要有以下个类组成:

  • NSManagedObjectModel
  • NSPersistentStore
  • NSPersistentStoreCoordinator
  • NSManagedObjectContext

NSManagedObjectModel:代表了数据模型,也就是Xcode中创建的.xcdatamodel文件中所表诉的信息。
NSPersistentStore:是数据存放的地方,可以是SQLite、XML(仅OS X)、二进制文件、或仅内存中。
NSPersistentStoreCoordinator:是协调者,用来在NSManagedObjectContext和NSPersistentStore之间建立连接。
NSManagedObjectContext:是应用程序唯一需要访问的类,其中提供了对数据操作的一系列接口。

另外Core Data中最常用的类就是NSFetchRequest和NSManagedObject。
NSFetchRequest用来执行查询,NSManagedObject就是模型对象。

废话不多说,下面来分条介绍Core Data具有哪些特性。

1. 图形用户界面

Xcode为Core Data提供的图形界面是其方便使用的原因之一,使用图形界面可以做以下事情:

  1. 创建实体极其属性
  2. 创建关联
  3. 查看对象图
  4. 设置二进制数据分离存储
  5. 完成常见的数据迁移操作

等等……

2. Fetch Template

对于一些固定的Fetch操作,可以用Xcode图形化的创建Fetch Template,来减少代码的编写。

通过FetchTemplate创建FetchRequest

1
let fetchRequest = managedObjectModel.fetchRequestTemplateForName("templateName")

3. Fetch Result Type

Fetch数据的结果可以很方便的按需求选择类型,可以以一下四种类型呈现:

  • NSManagedObjectResultType:直接返回NSManagedObject对象
  • NSCountResultType:返回总数,出于性能考虑,在只需要总数不需要具体对象数据时,用这个类型。
  • NSDictionaryResultType:将数据用字典类型返回。
  • NSManagedObjectIDResultType:返回所有NSManagedObject对象的唯一标识符

4. NSPredicate

NSPredicate和NSFetchRequest配合可以实现各种各样需求的查询,非常强大。

1
2
let dogFetch = NSFetchRequest(entityName: "Dog")
dogFetch.predicate = NSPredicate(format: "name == %@", dogName)

以上代码为查询name为dogName变量值的Dog对象

出于性能考虑,有多个条件时,应该把最简单的条件放到最前,例如:
(salary > 5000000) AND (lastName LIKE 'Quincey')
的性能要比
(lastName LIKE 'Quincey') AND (salary > 5000000)
好。

5. Data Validation

Core Data的大部分类型都能通过图形界面方便的设置Validation

比如上图,图中的属性是一个Double类型,可以用面板为它设置最大值最小值。当保存的值不在这个范围内时,会抛出一个异常。

也可以在NSManagedObject的子类中实现validate<属性名>:方法来自定义Validation

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func validateAge(value: AutoreleasingUnsafeMutablePointer<AnyObject?>) throws {
if value == nil {
return
}

let valueNumber = value.memory as! NSNumber
if valueNumber.floatValue > 0.0 {
return
}
let errorStr = NSLocalizedString("Age must be greater than zero", tableName: "Employee", comment: "validation: zero age error")
let userInfoDict = [NSLocalizedDescriptionKey: errorStr]
let error = NSError(domain: "EMPLOYEE_ERROR_DOMAIN", code: 1123, userInfo: userInfoDict)
throw error
}

也可以实现validateForInsert:validateForUpdate:validateForDelete:方法来在,数据插入、更新、删除等操作时进行Validation,这里就不细讲了。

6. NSFetchedResultController

因为Core Data中的数据大部分都会由UITableView来展示,苹果工程师开发了NSFetchedResultController这个组件来使得写很少的代码就能让数据和UITableView绑定,并且保证了良好的性能。

创建NSFetchedResultController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func initializeFetchedResultsController() {
let request = NSFetchRequest(entityName: "Person")
let departmentSort = NSSortDescriptor(key: "department.name", ascending: true)
let lastNameSort = NSSortDescriptor(key: "lastName", ascending: true)
request.sortDescriptors = [departmentSort, lastNameSort]

let moc = self.dataController.managedObjectContext
self.fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: moc, sectionNameKeyPath: "department.name", cacheName: "rootCache")
self.fetchedResultsController.delegate = self

do {
try self.fetchedResultsController.performFetch()
} catch {
fatalError("Failed to initialize FetchedResultsController: \(error)")
}
}

和UITableView绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func configureCell(cell: UITableViewCell, indexPath: NSIndexPath) {
let employee = self.fetchedResultsController.objectAtIndexPath(indexPath) as! AAAEmployeeMO
// Populate cell from the NSManagedObject instance
print("Object for configuration: \(object)")
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier", forIndexPath: indexPath) as! UITableViewCell
// Set up the cell
self.configureCell(cell, indexPath: indexPath)
return cell
}

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.fetchedResultsController.sections!.count
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sections = self.fetchedResultsController.sections as! [NSFetchedResultsSesionInfo]
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}

UITableView监听数据变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
case .Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
case .Move:
break
case .Update:
break
}
}

func controller(controller: NSFetchedResultsController, didChangeObject anObject: NSManagedObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
case .Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Update:
self.configureCell(self.tableView.cellForRowAtIndexPath(indexPath!)!, indexPath: indexPath!)
case .Move:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
self.tableView.insertRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
}
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.tableView.endUpdates()
}

7. 异步Fetch

有时Fetch数据的时间会很长,为了避免其阻塞UI,可以使用异步Fetch。这是iOS 8新增的API——NSAsynchronousFetchRequest。不要被它的名字误导了,它并不继承于NSFetchRequest,而是继承于NSPersistentStoreRequest。

1
2
3
4
5
6
7
8
9
10
11
12
fetchRequest = NSFetchRequest(entityName: "Employee")

asycnFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { [unowned self] result in
self.employees = result.finalResult as! [Employee]
self.tableView.reloadData()
}

do {
try context.executeRequest(asycnFetchRequest)
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}

如上代码所示,通过NSAsynchronousFetchRequest可以异步的执行一个NSFetchRequest,并在执行结束后回调闭包。

8. Auto Migration

Core Data对一些常见的,不是特别复杂的结构变动支持自动迁移。
一些比较复杂的结构变动也可以在Xcode图形界面中去编辑迁移规则,这样也可以不写代码实现数据迁移。
对于特别复杂的,就自己编写代码来手工迁移吧。

数据迁移部分将来会单独研究。

9. NSUndoManager

Core Data支持用NSUndoManager来回退操作。

10. iCloud支持

Core Data支持方便的将全部或部分数据同步到iCloud中。如果APP中有这样的需求,那么所有本地数据存储方案中,Core Data肯定是首选。

11. Batch Update

有时,我们希望对所有对象的某一个属性值进行改变,也就是批量更新。如果先把这些对象从Core Data中全部读取出来,遍历改变他们的值,然后保存更改,这种操作费时又费性能。
iOS 8后引进了Batch Update,这种方式可以批量更新Core Data中的对象而不需要把它们读入内存。
Batch Update不需要经过NSManagedObjectContext,而是直接操作NSPersistenceStore。

1
2
3
4
5
6
7
8
9
10
11
12
13
let batchUpdate = NSBatchUpdateRequest(entityName: "Employee")

batchUpdate.propertiesToUpdate = ["favorite": NSNumber(bool: true)]
batchUpdate.affectedStores = context.persistentStoreCoordinator!.persistentStores

batchUpdate.resultType = .UpdatedObjectsCountResultType

do {
let batchResult = try context.executeRequest(batchUpdate) as! NSBatchUpdateResult
print("Records updated \(batchResult.result!)")
} catch let error as NSError {
print("Could not update \(error), \(error.userInfo)")
}

如上面代码,为Employee这个实体创建一个NSBatchUpdateRequest对象,通过propertiesToUpdate来设置要改变的属性名和属性值。
通过设置affectedStores来决定要在哪些NSPersistenceStore上生效。

12. Batch Delete

NSBatchDeleteRequest是iOS 9新增的API,用法和NSBatchUpdateRequest差不多,它会直接从PersistenceStore中删除数据,省去先载入到内存,删除,再写回Store的过程。

13. NSExpression

NSExpression也可以和NSFetchRequest配合使用,使用NSExpression内置的一些函数,例如求和“sum:”,求平均数“average:”等等
可以在查询数据的时候带来很好的性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let fetchRequest = NSFetchRequest(entityName: "Venue")

fetchRequest.resultType = .DictionaryResultType

let sumExpressionDesc = NSExpressionDescription()
sumExpressionDesc.name = "sumDeals"

sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments:[NSExpression(forKeyPath: "specialCount")]) sumExpressionDesc.expressionResultType = .Integer32AttributeType
fetchRequest.propertiesToFetch = [sumExpressionDesc]

do {
let results = try coreDataStack.context
.executeFetchRequest(fetchRequest) as! [NSDictionary]
let resultDict = results.first!
let numDeals = resultDict["sumDeals"]
numDealsLabel.text = "\(numDeals!) total deals"
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}

14. Instruments

Xcode的Instruments中带有专门调试Core Data的工具,帮助排查使用Core Data遇到的问题。

15. Private Queue Context

当执行一些耗时较长的操作时,例如插入、更新大量数据,用主队列Context会阻塞UI,于是需要将这些操作放到非主线程去做。而Core Data的Context执行的操作都不是线程安全的,如果用GCD这种去将操作放在后台执行会出现不可预期的问题。
Core Data为解决这个问题提供了Private Queue Context,也就是后台Context,可以让后台Context去执行这些操作,从PrivateQueue这个名字上也能看出,这个Queue只有创建它的Context可以访问,Context执行的操作都会限制在这一个Queue上,避免线程安全问题。

1
2
3
4
5
6
7
8
9
10
11
12

let privateContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateContext.persistentStoreCoordinator = coreDataStack.context.persistentStoreCoordinator

privateContext.performBlock {
let results: [AnyObject]
do {
results = try self.coreDataStack.context .executeFetchRequest(self.surfJournalFetchRequest())
} catch {
let nserror = error as NSError print("ERROR: \(nserror)") results = []
}
}

看上面代码,通过给concurrencyType传入.PrivateQueueConcurrencyType即可创建一个PrivateQueueContext,把MainQueueContext的persistentStoreCoordinator赋给它,即可让PrivateQueueContext也可以访问MainQueueContext下的持久化存储区。

在performBlock的末尾部分,可以向主队列追加一些操作来更新UI

1
2
3
4
5
6
7
8
9
	...

dispatch_async(dispatch_get_main_queue(), {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
print("Export Path: \(exportFilePath)") self.showExportFinishedAlertView(exportFilePath)
})

} // closing brace for performBlock()

16. Child Context

Core Data中的每一个NSManagedObjectContext都有一个父存储区(Parent Store),Context以这个父存储区作为持久化存储区(PersistenceStore)与之交互。
最上层Context的ParentStore就是NSPersistentStoreCoordinator。
每个子Context就以它的父Context为PersistenceStore。

可以为主Context创建一些Child Context去做一些临时的操作,操作执行完以后会保存到主Context,当主Context执行了保存时才会把数据真正保存到存储区。

还有一种用法是可以在程序中创建两个Context,一个主队列Context,一个PrivateQueueContext,然后令主队列Context作为PrivateQueueContext的子Context,这样主队列Context的操作会先保存到PrivateQueueContext中,再由PrivateQueueContext在后台慢慢的保存到PersistenceStore中,来保证界面的性能良好。

关于Child Context本文先不做详细研究。

17. Performance

Core Data的性能方面苹果是花了不少功夫的,比如说采用了faulting技术。除了iOS 7下批量更新的问题,iOS 8以上的Core Data性能应该是不错的。在Core Data的使用中,有很多优化点可以保证良好的性能,本文暂先不做介绍了。