2014年12月15日 星期一

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來做實驗,就會發現,當新增的資料愈多時,卡住的時間就會愈久。如以下程式:
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];
這個程式片斷使用預設的managed object context來作業,預設就是會卡住。

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;
這樣一來,原本新增資料的程式就可以改成:
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];   }
 其實只是把程式原本用到managedObjectContext改成使用childContext而已。為了更明顯的讓大家看出來,這段程式會跑在不同的thread,慣例上會用 performBlock:^{ ...  }來將程式放入。而這樣,何時會真正把資料存入檔案中去呢?最後一行的[childContext save],其實只是把資料由childContext中和其parent context同步而已。所以,還是要在parent context再做一次save的動作才會真正將資料存入檔案去。

2. 建立另一個和原managedObjectContext沒有父子關係的context,當然也要指定其跑在不同的Thread,如此使用此context時,就不會在UI Thread上跑。也就不會卡住UI了。配置如下圖:

程式寫法,在要用到讀寫core data時,直接寫。所以原本的新增資料程式改成如下:
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 save
            if (i % 200 == 0) {
                [importContext save:nil];
            }
        }
        [importContext save:nil];   }
原本是父子關係的Context, 在這裡改成有點手足關係的Context, 同樣的都共用同一個Coordinator, 同樣的都指定使用NSPrivateQueueConcurrencyType,以便使用不同的thread來執行。都可以達成不會卡住UI的目標。
可是這個時候,因為不是父子關係,兩個Context要同步,作法就不太一樣[3]:
[[NSNotificationCenter defaultCenter]
     addObserverForName:NSManagedObjectContextDidSaveNotification
     object:nil
     queue:nil
     usingBlock:^(NSNotification* note)
    {
        NSManagedObjectContext *moc = self.managedObjectContext;
        if (note.object != moc)
            [moc performBlock:^(){
                [moc mergeChangesFromContextDidSaveNotification:note];
            }];

    }];
那以上兩種不同的配置作法,要用那一個呢?其實都可以啦!不過,Florian Kugler[2]是說第二個作法是比較有效率的。

參考網址:
[1] Dave, adding-core-data-existing-iphone-projects/
[2] Florian Kugler, concurrent-core-data-stack-performance-shootout
[3] Chris Eidhof, common-background-practices.html

張貼留言