iOS Core Data and multithread
對於我們初學Core Data的人來說,在Xcode 5的時代,若要使用Core Data來存儲資料,第一件事就是在建立專案的時候只能選擇空的應用程式,才能有勾選使用Core Data的選項。如果選擇建立single view專案的話,那就沒有使用Core Data選項可選了。所以網路[1]上有不少人在討論,若已經建立專案,而當初沒有勾到Core Data選項時,該如何加入之。還好,到了XCode 6, 現在single view專案,已經有Core Data選項可以選了。
但是預設建立的Core Data, 在讀寫資料時,其實是以UI Thread來作業的。也就是說和所有UI有關的元件共用主要的Thread。這樣一來,最明顯的問題,就是在做Import/Export工作的時候,會造成App卡住的現象。
以簡單的新增資料數筆至Core Data來做實驗,就會發現,當新增的資料愈多時,卡住的時間就會愈久。如以下程式:
Google了一下,發現有幾個解決方法:
1. iOS8 有新的寫法可以使用
2. 在iOS5之後,就有針對這個問題,提出多緒的寫法
當然,為了相容於之前版本,現階段我還是採用2的方法。
這個方法,主要是將managed object context指定到不同的thread去,這樣就可以避開和UI Thread搶執行資源的問題。使用的方式又有兩個主要配置方法[2]:
1. 建立child context, 此時可以指定child context跑在不同的thread, 這樣就可以在不想卡住UI Thread的時候,將讀寫core data的工作交給這個child thread去做就好。而其他不會卡住UI的工作,還是可以用原來的方法來做。如下圖的配置:
程式部份,只要改寫原AppDelegate類別,加入:
2. 建立另一個和原managedObjectContext沒有父子關係的context,當然也要指定其跑在不同的Thread,如此使用此context時,就不會在UI Thread上跑。也就不會卡住UI了。配置如下圖:
程式寫法,在要用到讀寫core data時,直接寫。所以原本的新增資料程式改成如下:
可是這個時候,因為不是父子關係,兩個Context要同步,作法就不太一樣[3]:
參考網址:
[1] Dave, adding-core-data-existing-iphone-projects/
[2] Florian Kugler, concurrent-core-data-stack-performance-shootout
[3] Chris Eidhof, common-background-practices.html
但是預設建立的Core Data, 在讀寫資料時,其實是以UI Thread來作業的。也就是說和所有UI有關的元件共用主要的Thread。這樣一來,最明顯的問題,就是在做Import/Export工作的時候,會造成App卡住的現象。
以簡單的新增資料數筆至Core Data來做實驗,就會發現,當新增的資料愈多時,卡住的時間就會愈久。如以下程式:
這個程式片斷使用預設的managed object context來作業,預設就是會卡住。AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];Xxx *xx;for (int i=0; i
xx = [NSEntityDescription insertNewObjectForEntityForName:@"Xxx" inManagedObjectContext:delegate.managedObjectContext];xx.startDate = [NSDate date];xx.stopDate = [xx.startDate dateByAddingTimeInterval:(arc4random() % 7200 + 1)];xx.distance = @(5);}[delegate.managedObjectContext save:nil];
Google了一下,發現有幾個解決方法:
1. iOS8 有新的寫法可以使用
2. 在iOS5之後,就有針對這個問題,提出多緒的寫法
當然,為了相容於之前版本,現階段我還是採用2的方法。
這個方法,主要是將managed object context指定到不同的thread去,這樣就可以避開和UI Thread搶執行資源的問題。使用的方式又有兩個主要配置方法[2]:
1. 建立child context, 此時可以指定child context跑在不同的thread, 這樣就可以在不想卡住UI Thread的時候,將讀寫core data的工作交給這個child thread去做就好。而其他不會卡住UI的工作,還是可以用原來的方法來做。如下圖的配置:
程式部份,只要改寫原AppDelegate類別,加入:
並宣告:// child managedObjectContext whichs runs in a background thread- (NSManagedObjectContext *)childContext{if (_childContext != nil) {return _childContext;}NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];if (coordinator != nil) {_childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];[_childContext setParentContext:self.managedObjectContext];}return _childContext;}
這樣一來,原本新增資料的程式就可以改成:@property (readonly, strong, nonatomic) NSManagedObjectContext *childContext;
其實只是把程式原本用到managedObjectContext改成使用childContext而已。為了更明顯的讓大家看出來,這段程式會跑在不同的thread,慣例上會用 performBlock:^{ ... }來將程式放入。而這樣,何時會真正把資料存入檔案中去呢?最後一行的[childContext save],其實只是把資料由childContext中和其parent context同步而已。所以,還是要在parent context再做一次save的動作才會真正將資料存入檔案去。AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];[delegate.childContext performBlock:^{Xxx *xx;for (int i=0; i
xx = [NSEntityDescription insertNewObjectForEntityForName:@"Xxx" inManagedObjectContext:delegate.childContext];xx.startDate = [NSDate date];xx.stopDate = [step.startDate dateByAddingTimeInterval:(arc4random() % 7200 + 1)];xx.distance = @(5);}[delegate.childContext save:nil]; }
2. 建立另一個和原managedObjectContext沒有父子關係的context,當然也要指定其跑在不同的Thread,如此使用此context時,就不會在UI Thread上跑。也就不會卡住UI了。配置如下圖:
程式寫法,在要用到讀寫core data時,直接寫。所以原本的新增資料程式改成如下:
原本是父子關係的Context, 在這裡改成有點手足關係的Context, 同樣的都共用同一個Coordinator, 同樣的都指定使用NSPrivateQueueConcurrencyType,以便使用不同的thread來執行。都可以達成不會卡住UI的目標。AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];NSManagedObjectContext *importContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];NSPersistentStoreCoordinator *coordinator = delegate.managedObjectContext.persistentStoreCoordinator;[importContext setPersistentStoreCoordinator:coordinator];[importContext setUndoManager:nil];[importContext performBlock:^{Xxx *xx;for (int i=0; i
xx = [NSEntityDescription insertNewObjectForEntityForName:@"Xxx" inManagedObjectContext:importContext];xx.startDate = [NSDate date];xx.stopDate = [step.startDate dateByAddingTimeInterval:(arc4random() % 7200 + 1)];xx.distance = @(5);// context saveif (i % 200 == 0) {[importContext save:nil];}}[importContext save:nil]; }
可是這個時候,因為不是父子關係,兩個Context要同步,作法就不太一樣[3]:
那以上兩種不同的配置作法,要用那一個呢?其實都可以啦!不過,Florian Kugler[2]是說第二個作法是比較有效率的。[[NSNotificationCenter defaultCenter]addObserverForName:NSManagedObjectContextDidSaveNotificationobject:nilqueue:nilusingBlock:^(NSNotification* note){NSManagedObjectContext *moc = self.managedObjectContext;if (note.object != moc)[moc performBlock:^(){[moc mergeChangesFromContextDidSaveNotification:note];}];
}];
參考網址:
[1] Dave, adding-core-data-existing-iphone-projects/
[2] Florian Kugler, concurrent-core-data-stack-performance-shootout
[3] Chris Eidhof, common-background-practices.html
留言