写入 coredata,即使在后台线程上完成,也会严重阻塞 UI

Posted

技术标签:

【中文标题】写入 coredata,即使在后台线程上完成,也会严重阻塞 UI【英文标题】:Writing to coredata, even when done on background thread , blocks the UI considerably 【发布时间】:2013-06-03 13:17:24 【问题描述】:

我正在尝试一个基于聊天的应用程序。我已经设置了一个单例核心数据管理器,如下所示

#import "CGSharedCoreData.h"
CGSharedCoreData *_cd;


@implementation CGSharedCoreData

@synthesize managedObjectModel = managedObjectModel_;
@synthesize managedObjectContext = managedObjectContext_;
@synthesize persistentStoreCoordinator = persistentStoreCoordinator_;


+ (CGSharedCoreData *)sharedCoreData
    static CGSharedCoreData *_cd = nil;
    static dispatch_once_t onceCoreDataShared;
    dispatch_once(&onceCoreDataShared, ^
        _cd = [[CGSharedCoreData alloc] init];
    );
    return _cd;



+ (void)saveContext:(NSManagedObjectContext*)c

    @try 


        if (c.persistentStoreCoordinator.persistentStores.count == 0)
        
            // This is the case where the persistent store is cleared during a logout.
            CGLog(@"saveContext: PersistentStoreCoordinator is deallocated.");
            return;
        


        // Register context with the notification center
        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
        CGSharedCoreData *sharedCoreData = [CGSharedCoreData sharedCoreData];

        [nc addObserver:sharedCoreData
               selector:@selector(mergeChanges:)
                   name:NSManagedObjectContextDidSaveNotification
                 object:c];


        NSError *error = nil;
        if (c != nil) 
            CGLog(@"thread  &&*&(*(* %d",[NSThread isMainThread]);
            if ([c hasChanges] && ![c save:&error]) 
                CGLog(@"Unresolved error %@, %@", error, [error userInfo]);
            
        

        [nc removeObserver:sharedCoreData name:NSManagedObjectContextDidSaveNotification
                    object:c];

    

    @catch (NSException *exception) 
        CGLog(@"***** Unresolved CoreData exception %@", [exception description]);
    




+ (dispatch_queue_t) backgroundSaveQueue

    static dispatch_queue_t coredata_background_save_queue;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^
        coredata_background_save_queue = dispatch_queue_create("com.shashank.coredata.backgroundsaves", NULL);
    );

    return coredata_background_save_queue;



+ (void)performInTheBackground:(void (^)(NSManagedObjectContext *blockContext))bgBlock 

    dispatch_async([CGSharedCoreData backgroundSaveQueue],
    ^
        NSManagedObjectContext *newContext = [[NSManagedObjectContext alloc] init];
        [newContext setPersistentStoreCoordinator:CG_CORE_DATA.persistentStoreCoordinator]; // Create a managed object context
        [newContext setStalenessInterval:0.0];
        [newContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];

        bgBlock(newContext);

    );


- (void)saveContext 
    [CGSharedCoreData saveContext:managedObjectContext_];


- (void)clearStore 

    NSError *error = nil;

    if  (self.persistentStoreCoordinator) 
        if ([persistentStoreCoordinator_ persistentStores] == nil) 
            CGLog(@"No persistent stores to clear!");
        
        else 
            CGLog(@"Cleaning persistent stores!");

            managedObjectContext_ = nil;


            NSPersistentStore *store = [[persistentStoreCoordinator_ persistentStores] lastObject];

            if (![persistentStoreCoordinator_ removePersistentStore:store error:&error]) 
                CGLog(@"Unresolved error %@, %@", error, [error userInfo]);
                abort();
            

            // Delete file
            if ([[NSFileManager defaultManager] fileExistsAtPath:store.URL.path]) 
                if (![[NSFileManager defaultManager] removeItemAtPath:store.URL.path error:&error]) 
                    CGLog(@"Unresolved error %@, %@", error, [error userInfo]);
                    abort();
                
            

            // Delete the reference to non-existing store
            persistentStoreCoordinator_ = nil;
        
    


#pragma mark - Core Data stack

// Returns the managed object context for the application.
// If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
- (NSManagedObjectContext *)managedObjectContext

     NSAssert([NSThread isMainThread], @"Must be instantiated on main thread.");

    if (managedObjectContext_ != nil) 
        return managedObjectContext_;
    

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil)
    
        managedObjectContext_ = [[NSManagedObjectContext alloc] init];
        [managedObjectContext_ setPersistentStoreCoordinator:coordinator];
        [managedObjectContext_ setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
    

    return managedObjectContext_;


// Returns the managed object model for the application.
// If the model doesn't already exist, it is created from the application's model.

- (NSManagedObjectModel *)managedObjectModel


    if (managedObjectModel_ != nil) 
        return managedObjectModel_;
    

    managedObjectModel_ = [NSManagedObjectModel mergedModelFromBundles:nil];
    return managedObjectModel_;


// Returns the persistent store coordinator for the application.
// If the coordinator doesn't already exist, it is created and the application's store added to it.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator 

    if (persistentStoreCoordinator_ != nil) 
        return persistentStoreCoordinator_;
    

    CGLog(@"Creating a persistent store!");

    NSURL *storeURL = [NSURL fileURLWithPath: [[self applicationLibraryDirectory] stringByAppendingPathComponent: @"CGChatData.sqlite"]];

    NSError *error = nil;
    persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) 



        CGLog(@"Unresolved error %@, %@", error, [error userInfo]);
        //        abort();
        CGLog(@"Delete STORE: %d",[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]);
        persistentStoreCoordinator_ = nil;
        persistentStoreCoordinator_ = self.persistentStoreCoordinator;
        

    return persistentStoreCoordinator_;


#pragma mark -
#pragma mark Handling Multiple Contexts

/**
 Merges the changes from the insert contexts to the main context on the main thread.
 */

- (void)mergeChanges:(NSNotification *)notification

    // Merge changes into the main context on the main thread
    if(![[CGLoginEngine sharedLoginEngine] isLoggedIn])
        return;

    if (![NSThread isMainThread])
    
        [self performSelectorOnMainThread:@selector(mergeChanges:)
                               withObject:notification waitUntilDone:YES];
        return;
    

    //CAUTION: Without the for clause below the NSFetchedResultsController may not capture all the changed objects.
    //For more info see: http://***.com/questions/3923826/nsfetchedresultscontroller-with-predicate-ignores-changes-merged-from-different
    //and http://***.com/questions/2590190/appending-data-to-nsfetchedresultscontroller-during-find-or-create-loop


    for (NSManagedObject *object in [[notification userInfo] objectForKey:NSUpdatedObjectsKey])
    
        [[managedObjectContext_ objectWithID:[object objectID]] willAccessValueForKey:nil];
    


    [managedObjectContext_ mergeChangesFromContextDidSaveNotification:notification];


现在,每当我需要发送消息时,我都会将其写入核心数据并使用获取的结果控制器更新 tableview。写作部分是这样的:

[CGSharedCoreData performInTheBackground:^(NSManagedObjectContext *blockContext)
         
             CGLog(@"thread ******* is main %d",[NSThread isMainThread]);
            [[CGChatsModelController sharedChatModel] addChatWithtext:[chatMessage objectForKey:@"Message"]
                                                                              username:[chatMessage objectForKey:@"Receiver"]
                                                                             firstName:[chatMessage objectForKey:@"ReceiverFirstName"]
                                                                              lastName:[chatMessage objectForKey:@"ReceiverLastName"]
                                                                               imageId:[chatMessage objectForKey:@"ReceiverImageID"]
                                                                           createdByMe:YES
                                                                                  time:time
                                                                               context:blockContext];
              [CGSharedCoreData saveContext:blockContext];

但是,当我在很短的时间内发送多条消息时,它会完全阻塞我的 UI,即使核心数据保存和所有其他相关操作正在后台队列中完成。 发生这种情况有什么特别的原因吗?

我在这里附上我的代码的其他几个块以供参考:

- (CGChat *) addChatWithtext:(NSString *)text username:(NSString *)username firstName:(NSString *)firstName lastName:(NSString *)lastName imageId:(NSString *)imageId createdByMe:(BOOL)yesOrNo time:(NSDate *)date context:(NSManagedObjectContext *)context

    NSManagedObjectContext *backgroundContext = context;
    CGChat *chat = (CGChat *)[NSEntityDescription insertNewObjectForEntityForName:@"CGChat" inManagedObjectContext:backgroundContext];
    chat.text = text;
    chat.createdByMe = [NSNumber numberWithBool:yesOrNo];


     chat.status = @"sent";


    [self addChat:chat toUserWithUserName:username firstName:firstName lastName:lastName imageID:imageId time:date WithContext:backgroundContext];

    return chat;



- (CGChat *)lookUpChatWithUserName:(NSString *)username text:(NSString *)text timeStamp:(NSString *)timeStamp createdByMe:(BOOL) yesOrNo context:(NSManagedObjectContext *)context


    NSFetchRequest *request = [[NSFetchRequest alloc] init];

    NSEntityDescription *entity = [NSEntityDescription entityForName:@"CGChat" inManagedObjectContext:context];
    [request setEntity:entity];

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"userIchatWith == %@ && text == %@ && timeStamp == %@ && createdByMe ==%@", [self lookUpForUserWithUsername:username inContext:context],text,timeStamp,[NSNumber numberWithBool:yesOrNo]];

    [request setPredicate:predicate];

    NSArray *resultArray = nil;
    NSError *error;

    resultArray = [context executeFetchRequest:request error:&error];

    CGChat *chat = nil;
    if ([resultArray count] > 0) 
        chat  = [resultArray objectAtIndex:0];
    
    return chat;




- (void) addChat:(CGChat *)chat toUserWithUserName:(NSString *)username firstName:(NSString *)firstName lastName:(NSString *)lastName imageID:(NSString *)imageId time:(NSDate *)date WithContext:(NSManagedObjectContext *)context



    CGUser *user = [self lookUpForUserWithUsername:username inContext:context];

    if (!user)
    
        user = (CGUser *)[NSEntityDescription insertNewObjectForEntityForName:@"CGUser" inManagedObjectContext:context];
        user.userName = username ;


    
    user.firstName = firstName;
    user.lastName = lastName;

    if (![user.imageID isEqualToString:imageId])
    
        user.imageID = imageId;
    


    CGChat *chats = [self getLastChatForUsername:username andContext:context];
    if(chats)
    
        chats.isLastChat = [NSNumber numberWithBool:NO];
    


    NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
    [dateFormat setDateFormat:@"yyyy-MM-dd HH:mm:ss"];

    chat.timeStamp = [dateFormat stringFromDate:date];
    chat.isLastChat = [NSNumber numberWithBool:YES];
    chat.userIchatWith = user;
    user.timeOfLatestChat = [dateFormat stringFromDate:date];




- (CGChat *) getLastChatForUsername:(NSString *)username andContext:(NSManagedObjectContext *)context


    CGUser *user = [self lookUpForUserWithUsername:username inContext:context];

    if(user)
    
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"isLastChat == %@",[NSNumber numberWithBool:YES]];

        NSSet *filteredSet = [user.chats filteredSetUsingPredicate:predicate];

        return [[filteredSet allObjects] lastObject];
    
    else
    
        return nil;
    



- (CGUser *) lookUpForUserWithUsername:(NSString *)username inContext:(NSManagedObjectContext *)context

    NSFetchRequest *request = [[NSFetchRequest alloc] init];

    NSEntityDescription *entity = [NSEntityDescription entityForName:@"CGUser" inManagedObjectContext:context];
    [request setEntity:entity];

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"userName == %@", username];
    [request setPredicate:predicate];

    NSArray *resultArray = nil;
    NSError *error;

    resultArray = [context executeFetchRequest:request error:&error];

    CGUser *user = nil;
    if ([resultArray count] > 0) 
        user  = [resultArray objectAtIndex:0];
    

    return user;

【问题讨论】:

【参考方案1】:

这里有几个问题:

    Core Data 不会抛出 NSException,不要将您的 Core Data 调用包装在 try/catch 中。如果 else 确实引发了异常,那只会产生噪音。 try/catch 在 Objective-C 编程中非常很少使用。

    当您使用父/子上下文设计时,您正在侦听和合并来自 NSManagedObjectContextDidSaveNotification 调用的更改。这是不正确的。每当您保存子上下文时,父/子关系都会自动为您处理此问题。当您侦听并使用该通知时,您将强制 Core Data 在主线程上第二次处理该信息。

    由于您没有显示调用 -add... 方法的代码,因此不清楚您传递的是什么“背景上下文”。背景上下文不应持续很长时间。它们确实是要被使用和销毁的。背景上下文存在的时间越长,它就会离主上下文越远,因为主上下文中的更改不会传递给子上下文。

更新

抱歉,我误读了代码。由于您将后台线程的更改合并到主线程中,因此无法避免阻塞主线程。这是创建父/子设计的主要原因之一。

避免这种情况的唯一方法是:

    新的NSPersistentStoreCoordinator指向同一个sqlite文件 从后台线程更新 NSPersistentStoreCoordinator 通知主NSManagedObjectContext重新加载所有数据

这将避免大部分主线程阻塞,但代码复杂性的开销几乎总是太多。

【讨论】:

@marcus : ** 当您使用父/子上下文设计时,您正在侦听和合并来自 NSManagedObjectContextDidSaveNotification 调用的更改。这是不正确的。每当您保存子上下文时,父/子关系都会自动为您处理此问题。** 我没有使用父子上下文类型(嵌套上下文)。而是使用 pre-ios 5 类型的 coredata 合并。 “后台上下文”是每次我在后台队列中执行任何进程时分配的新上下文。我在code + performInBackground 方法中提到的code @Marcus S. Zarra:这是供您参考的代码。 + (void)performInTheBackground:(void (^)(NSManagedObjectContext *blockContext))bgBlock dispatch_async([CGSharedCoreData backgroundSaveQueue], ^ NSManagedObjectContext *newContext = [[NSManagedObjectContext alloc] init]; [newContext setPersistentStoreCoordinator:CG_CORE_DATA.persistentStoreCoordinator]; / / 创建一个托管对象上下文 [newContext setStalenessInterval:0.0]; [newContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy]; bgBlock(newContext); ); @Marcus S Zarra:谢谢。会试试这个。

以上是关于写入 coredata,即使在后台线程上完成,也会严重阻塞 UI的主要内容,如果未能解决你的问题,请参考以下文章

CoreData 在后台 MOC 的保存操作期间无法完成故障

为啥即使在指定返回 - 后台线程到主线程问题后代码也会执行?

CoreData 多线程死锁

在后台线程上将 XML 解析为 CoreData 以不锁定 UI

无法永远分离线程c ++

CoreData prepareForDeletion 调用无限次