文章目录
  1. 1. Batch Updates
  2. 2. Asynchronous Fetching

随着iOS8和OSX10.10的发布,Core Data也迎来了更新。这次的更新可谓是重量级的,它使得程序员能够更加直接高效的操作数据库,在处理大量数据时速度明显提升(这在以前不知有多少程序员因为Core Data批量更新数据效率之低而不得不放弃使用它)。Batch Updates可用于批量快速更新数据,Asynchronous Fetching可用于异步抓取海量数据,并可以通过NSProgress实现进度跟踪和取消。

Batch Updates

在CoreData中想要更新大量数据,我们往往要将大量修改后的NSManagedObject加载到NSManagedObjectContext中并保存,这会占用大量内存,试想想在iPhone这样的内存有限的移动设备上将是个灾难,数据有可能丢失。你可能会采取批处理的方式,即一小批一小批的更新NSManagedObject并保存到NSManagedObjectContext中,但这样会花费很多时间,用户体验较差。

为了解决这个问题,苹果在NSManagedObjectContext加入了一个新的方法:executeRequest:error:,它接受一个NSPersistentStoreRequest类型的参数,返回类型为NSPersistentStoreResult

关于NSPersistentStoreRequest有些人可能比较熟悉,它是NSFetchRequestNSSaveChangesRequestNSBatchUpdateRequestNSAsynchronousFetchRequest的基类。后两个类是这次iOS8新加的,也是这篇文章将要讨论的内容。

NSPersistentStoreResult是一个新加入的类,它也是一个基类,而且是抽象类,这个类作为executeRequest:error:返回内容的父类,相当于一个接口,它目前有两个子类:NSPersistentStoreAsynchronousResultNSBatchUpdateResult

你大概猜到了,NSBatchUpdateResult对应着前面的NSBatchUpdateRequest.下面举个栗子:

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
35
36
37
func resetWeight(sender: AnyObject) {
// Create Entity Description
let batchUpdateRequest = NSBatchUpdateRequest(entityName: "Choice")

// Configure Batch Update Request
batchUpdateRequest.resultType = NSBatchUpdateRequestResultType.UpdatedObjectIDsResultType
batchUpdateRequest.propertiesToUpdate = ["weight":1]
// batchUpdateRequest.affectedStores = []
// batchUpdateRequest.predicate = ...

// Execute Batch Request
var batchUpdateRequestError:NSError? = nil
var batchUpdateResult = managedObjectContext?.executeRequest(batchUpdateRequest, error: &batchUpdateRequestError) as! NSBatchUpdateResult
if batchUpdateRequestError != nil {
println("Unable to execute batch update request.")
println("\(batchUpdateRequestError)\(batchUpdateRequestError?.localizedDescription)")
}
else {
// Extract Object IDs
let objectIDs = batchUpdateResult.result as! [NSManagedObjectID]

for objectID in objectIDs {
// Turn Managed Objects into Faults
if var managedObject = managedObjectContext?.objectWithID(objectID) {
managedObjectContext?.performBlock({ () -> Void in
managedObjectContext?.refreshObject(managedObject, mergeChanges: false)
})
}
}
// Perform Fetch
var fetchError: NSError? = nil
if !fetchedResultsController.performFetch(&fetchError) {
println("Unable to perform fetch.")
println("\(fetchError)\(fetchError?.localizedDescription)")
}
}
}

这段代码来自HardChoice的DetailViewController.swift文件,用于批量重置权重.

先来说说NSBatchUpdateRequest。它有点像NSFetchRequest:它允许你指定一个想要更新数据的实体;也可以指定一个affectedStores,它存储了一个接受更新请求的NSPersistentStore数组。(其实它是NSPersistentStoreRequest的属性);它也有一个谓词属性predicate来做更新的条件,它跟NSFetchRequest中的谓词一样强大和灵活,类似于SQL的where语句;你需要指定想要更新的字段,通过propertiesToUpdate属性来描述字段更新,它是一个字段,key为NSPropertyDescription或属性名字符串,value为NSExpression或常量。在这里我选择的 Model 是Choice,它包含一个字符串类型的name字段和整型的weight字段.我想要将所有的weight字段都改为1;resultType属性是类型为NSBatchUpdateRequestResultType的枚举变量,默认为StatusOnlyResultType(什么都不返回),我在这里选择UpdatedObjectIDsResultType(返回被更新数据的 ID),当然你也可以选择UpdatedObjectsCountResultType来让结果返回更新记录的行数:

1
2
3
4
5
6
7
8
 // Create Entity Description
let batchUpdateRequest = NSBatchUpdateRequest(entityName: "Choice")

// Configure Batch Update Request
batchUpdateRequest.resultType = NSBatchUpdateRequestResultType.UpdatedObjectIDsResultType
batchUpdateRequest.propertiesToUpdate = ["weight":1]
// batchUpdateRequest.affectedStores = []
// batchUpdateRequest.predicate = ...

然后用之前提过苹果新加的新方法executeRequest:error:来执行 request 并获取 result:

1
2
3
// Execute Batch Request
var batchUpdateRequestError:NSError? = nil
var batchUpdateResult = managedObjectContext?.executeRequest(batchUpdateRequest, error: &batchUpdateRequestError) as! NSBatchUpdateResult

接着谈谈NSBatchUpdateResult,它有一个result属性和resultType属性,result中的内容类型resultType跟我们之前在NSBatchUpdateRequest设置过的resultType属性是对应的,可能是成功或者失败,有可能是每行被更新的ID,也可能是被更新的行数。

需要注意的是,由于NSBatchUpdateRequest并不会先将数据存入内存,而是直接操作数据库,所以并不会引起NSManagedObjectContext的同步更新,所以你不仅需要获取NSBatchUpdateResult然后刷新NSManagedObjectContext对应的数据和UI界面,还需要保证更新后的数据满足数据库模型上的validation,因为NSManagedObjectContext没有感知Batch Updates,一些数据验证工作就落在了程序员的身上(你需要写一段代码验证更新后的数据是合法的,用户可不希望在跑步APP上看到自己今天跑步里程是个负数)。一旦有非法数据录入数据库,下次加载并修改NSManagedObject的时候就会导致数据验证失败。除了上面提到的这些,还要注意Batch Updates对数据库的操作是乐观锁,也就是假定很少会发生同时存取同一块数据的情况,所以你需要制定一个合理的”merge”策略来应付因同时更新数据产生的冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Extract Object IDs
let objectIDs = batchUpdateResult.result as! [NSManagedObjectID]

for objectID in objectIDs {
// Turn Managed Objects into Faults
if var managedObject = managedObjectContext?.objectWithID(objectID) {
managedObjectContext?.performBlock({ () -> Void in
managedObjectContext?.refreshObject(managedObject, mergeChanges: false)
})
}
}
// Perform Fetch
var fetchError: NSError? = nil
if !fetchedResultsController.performFetch(&fetchError) {
println("Unable to perform fetch.")
println("\(fetchError)\(fetchError?.localizedDescription)")
}

上面的代码先是从结果中取到了所有被更新数据的 ID, 再根据这些 ID 获取对应的 NSManagedObject,并使其过期失效,强制更新数据.这里关键的是下面这句:

1
2
3
managedObjectContext?.performBlock({ () -> Void in
managedObjectContext?.refreshObject(managedObject, mergeChanges: false)
})

在 Swift 中如果不采用异步执行 block 的策略, UI 就不会更新.但在 Objective-C 上可以不用performBlock方法,直接调用refreshObject: mergeChanges:方法就行.

最后看看效果,点击红色的重置权重按钮,所有选项右侧都变成1:

重置权重

Batch Updates的优势在于其效率,在处理上万条数据的时候,它执行的时间跟SQL语句执行时间相当。毕竟它绕开了NSManagedObjectContext直接修改底层数据库,节省内存,但千万别忘了手动更新 UI.

Asynchronous Fetching

Asynchronous Fetching的加入依然是为了解决CoreData读取海量数据所带来的问题。通过使用Asynchronous Fetching,我们可以在抓取数据的同时不阻塞占用NSManagedObjectContext,并可以随时取消抓取行为,随时跟踪抓取数据的进度。

设想我们平时用NSFetchRequest抓取数据的时候,我们会先用NSManagedObjectContextexecuteFetchRequest:error:方法传入一个NSFetchRequest,然后请求会被发送到NSPersistentStore,然后执行一段时间后返回一个数组,在NSManagedObjectContext更新后,这个数组被当做executeFetchRequest:error:的返回值返回到我们这里。

而Asynchronous Fetching则不同,当我们将一个NSAsynchronousFetchRequest对象传入executeRequest:error:方法后会立即返回一个“未来的”NSAsynchronousFetchResultNSAsynchronousFetchRequest初始化时需要传入两个参数赋值给属性:

  1. completionBlock属性,允许我们在抓取完成后执行回调block;
  2. fetchRequest属性,类型是NSFetchRequest。也即是说虽然是异步抓取,其实我们用的还是以前的NSFetchRequest,当NSFetchRequest抓取结束后会更新NSManagedObjectContext,这也就意味着NSManagedObjectContext的并发类型只能是NSPrivateQueueConcurrencyTypeNSMainQueueConcurrencyType

于是当我们用NSAsynchronousFetchRequest抓取数据时,我们会先用NSManagedObjectContextexecuteRequest:error:方法传入一个NSAsynchronousFetchRequest,这个方法在NSManagedObjectContext上执行时,NSManagedObjectContext会立即制造并返回一个NSAsynchronousFetchResult,同时NSAsynchronousFetchRequest会被发送到NSPersistentStore。你现在可以继续编辑这个NSManagedObjectContext中的NSManagedObject,等到NSPersistentStore执行请求完毕时会将结果返回给NSAsynchronousFetchResultfinalResult属性,更新NSManagedObjectContext,执行NSAsynchronousFetchRequest的回调block。

举个栗子:

1
2
3
4
5
6
7
let request = NSFetchRequest(entityName: "MyEntity")
let async = NSAsynchronousFetchRequest(fetchRequest: request){
(id result) in
if result.finalResult {
//TODO..
}
}

Swift代码很简洁,并用了尾随闭包语法,看不懂的朋友也不用着急,知道NSAsynchronousFetchRequest大概的用法就行。

之前提到过NSAsynchronousFetchRequest能在抓取数据的过程中跟踪进度,于是乎NSProgress登场了!一行代码顶十句话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let request = NSFetchRequest(entityName: "MyEntity")
var asyncResult:NSPersistentStoreResult!
let async = NSAsynchronousFetchRequest(fetchRequest: request){
(id result) in
if result.finalResult {
//TODO..
}
}
let progress = NSProgress(totalUnitCount: 1)
progress.becomeCurrentWithPendingUnitCount(1)
managedObjectContext?.performBlock{
[unowned self] in
let error = NSErrorPointer()
asyncResult = self.managedObjectContext?.executeRequest(async, error: error)
}
progress.resignCurrent()

而取消获取数据只需要取消NSProgress就可以了!取消行为会沿着数的根节点蔓延到叶子。

1
progress.cancel()

可以在cancellationHandler属性设置取消后执行的block,这里不再多说。

文章目录
  1. 1. Batch Updates
  2. 2. Asynchronous Fetching