读 MagicalRecord 源码记录

MagicalRecord 这个库,用过CoreData的人都应该听说过它吧。有人说 CoreData 巨坑,有人说是坑也得跳。但是,用上了 MagicalRecord 之后,也许你能躺着过坑。既然项目用到了CoreData,那就来看下MagicalRecord的源码吧,我阅读的版本是2.3.2。

MagicalRecord 的好处


1, 清理你的CoreData相关代码,即它帮你省掉一大部分CoreData的代码编写
2, 简单清晰,一行代码就可以查询数据
3, 当需要优化查询数据的时候,可以对NSFetchRequest进行修改

MagicalRecord 怎么帮我们省掉CodeData的代码呢?


  • 先来回顾一下CoreData相关的概念与对象,不多说,请看图:
读 MagicalRecord 源码记录_第1张图片
来源于objc.io的图片stack-complex.png
  - NSManageObject:实体对象
  - NSManageObjectContext: 管理实体对象的上下文,会跟踪记录新增删除或修改的对象
  - NSPersistent Store: 对应于数据库;
  - NSPersistent StoreCoordinator:存储协调器,NSManageObjectContext不用直接与数据库打交道,交给协调器去处理就行了。
  • 那么,常规的CoreData使用是这样的:
    1, 先获取NSManagedObjectContext,我们平时都是通过NSManagedObjectContext来管理操作NSManagedObject实体对象的,大致过程是:加载管理对象模型文件->创建持久化存储协调器->指定数据存储的路径及存储类型->创建管理对象上下文并指定存储协调器,代码如下:
- (NSManagedObjectContext *)managedObjectContext {
    NSManagedObjectContext *context;
    //打开模型文件,参数为nil时则打开包中所有模型文件并合并成一个
    NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];
    //创建持久化存储协调器
    NSPersistentStoreCoordinator *storeCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    //创建数据库保存路径
    NSString *dir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    NSString *path = [dir stringByAppendingPathComponent:@"MyApplication.sql"];
    NSURL *url = [NSURL fileURLWithPath:path];
    //添加SQLite类型的持久存储到持久化存储协调器
    NSError *error;
    [storeCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error];
    if(error){
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    }else{
        // 创建管理对象上下文并指定存储协调器
        context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        context.persistentStoreCoordinator = storeCoordinator;
    }
    return context;
}

而使用MagicalRecord的话,获取NSManagedObjectContext极其方便,方法也很多,简单的例如:

self.managedObjectContext = [NSManagedObjectContext MR_defaultContext];

但在获取NSManagedObjectContext之前,一般在app启动初始化的时候,先要初始化Core Data堆栈,一句话搞掂,例如:

    [MagicalRecord setupCoreDataStackWithStoreNamed:@"MyApplication.sqlite"];

这里面做了什么呢?看一下:

+ (void) setupCoreDataStackWithStoreNamed:(NSString *)storeName
{
    if ([NSPersistentStoreCoordinator MR_defaultStoreCoordinator] != nil) return;
    // 第一步
    NSPersistentStoreCoordinator *coordinator = [NSPersistentStoreCoordinator MR_coordinatorWithSqliteStoreNamed:storeName];
    [NSPersistentStoreCoordinator MR_setDefaultStoreCoordinator:coordinator];//记录保存默认的协调器
    // 第二步
    [NSManagedObjectContext MR_initializeDefaultContextWithCoordinator:coordinator];
}

1)第一步其实就是创建存储协调器,指定数据存储的路径及存储类型:

+ (NSPersistentStoreCoordinator *) MR_coordinatorWithSqliteStoreNamed:(NSString *)storeFileName withOptions:(NSDictionary *)options
{
    NSManagedObjectModel *model = [NSManagedObjectModel MR_defaultManagedObjectModel];
    //创建协调器
    NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    //添加SQLite持久存储到协调器
    [psc MR_addSqliteStoreNamed:storeFileName withOptions:options];
    return psc;
}

这里要注意的是,创建协调器后,添加SQLite持久存储到协调器中的一些细节:

- (NSPersistentStore *) MR_addSqliteStoreNamed:(id)storeFileName configuration:(NSString *)configuration withOptions:(__autoreleasing NSDictionary *)options
{
    NSURL *url = [storeFileName isKindOfClass:[NSURL class]] ? storeFileName : [NSPersistentStore MR_urlForStoreName:storeFileName];
    NSError *error = nil;
    //如果保存目录不存在则创建
    [self MR_createPathToStoreFileIfNeccessary:url];
    //添加SQLite持久存储到解析器
    NSPersistentStore *store = [self addPersistentStoreWithType:NSSQLiteStoreType
                                                  configuration:configuration
                                                            URL:url
                                                        options:options
                                                          error:&error];
    //存储文件不存在(即数据库不存在)
    if (!store)
    {   //如果对象模型不匹配,则删除原有的存储文件,创建新的存储文件
        if ([MagicalRecord shouldDeleteStoreOnModelMismatch])
        {
            BOOL isMigrationError = (([error code] == NSPersistentStoreIncompatibleVersionHashError) || ([error code] == NSMigrationMissingSourceModelError) || ([error code] == NSMigrationError));
            if ([[error domain] isEqualToString:NSCocoaErrorDomain] && isMigrationError)
            {
                [[NSNotificationCenter defaultCenter] postNotificationName:kMagicalRecordPSCMismatchWillDeleteStore object:nil];
                
                NSError * deleteStoreError;
                // Could not open the database, so... kill it! (AND WAL bits)
                NSString *rawURL = [url absoluteString];
                NSURL *shmSidecar = [NSURL URLWithString:[rawURL stringByAppendingString:@"-shm"]];
                NSURL *walSidecar = [NSURL URLWithString:[rawURL stringByAppendingString:@"-wal"]];
                [[NSFileManager defaultManager] removeItemAtURL:url error:&deleteStoreError];
                [[NSFileManager defaultManager] removeItemAtURL:shmSidecar error:nil];
                [[NSFileManager defaultManager] removeItemAtURL:walSidecar error:nil];
           
                ......省略部分.......
                // Try one more time to create the store
                store = [self addPersistentStoreWithType:NSSQLiteStoreType
                                           configuration:nil
                                                     URL:url
                                                 options:options
                                                   error:&error];
                if (store) {
                    [[NSNotificationCenter defaultCenter] postNotificationName:kMagicalRecordPSCMismatchDidRecreateStore object:nil];
                    // If we successfully added a store, remove the error that was initially created
                    error = nil;
                } else {
                    [[NSNotificationCenter defaultCenter] postNotificationName:kMagicalRecordPSCMismatchCouldNotRecreateStore object:nil userInfo:@{@"Error":error}];
                }
            }
        }
        [MagicalRecord handleErrors:error];
    }
    return store;
}

平时在开发过程中,如果修改了对象模型结构(如添加了模型的字段),需要把app卸载了然后重新安装才能避免打不开数据库导致崩溃的问题,这里的处理方式是如果发现模型不匹配,则根据 shouldDeleteStoreOnModelMismatch 变量来确定是否删掉原来的数据库,然后重新创建,为我们节省了很多时间。这是在MagicalRecordInternal文件中该类进行初始化的时候,shouldDeleteStoreOnModelMismatch变量就被设置为:在DEBUG模式下为YES,代码如下:

+ (void) initialize;
{
    if (self == [MagicalRecord class]) 
    {
        [self setShouldAutoCreateManagedObjectModel:YES];
        [self setShouldAutoCreateDefaultPersistentStoreCoordinator:NO];
#ifdef DEBUG
        [self setShouldDeleteStoreOnModelMismatch:YES];
#else
        [self setShouldDeleteStoreOnModelMismatch:NO];
#endif
    }
}

这一点,在MagicalRecord的官方指南文件里就有提到,这里。另外,在做CoreData数据迁移的时候,不希望MagicalRecord直接删除原有数据库,就可以设置shouldDeleteStoreOnModelMismatch这个参数.

2)紧接着第二步,创建设置NSManagedObjectContext:

+ (void) MR_initializeDefaultContextWithCoordinator:(NSPersistentStoreCoordinator *)coordinator;
{
    NSAssert(coordinator, @"Provided coordinator cannot be nil!");
    if (MagicalRecordDefaultContext == nil)
    {
        NSManagedObjectContext *rootContext = [self MR_contextWithStoreCoordinator:coordinator];
        [self MR_setRootSavingContext:rootContext];

        NSManagedObjectContext *defaultContext = [self MR_newMainQueueContext];
        [self MR_setDefaultContext:defaultContext];
        
        [defaultContext setParentContext:rootContext];
    }
}

这里创建了两个context,rootContext的并发类型是NSPrivateQueueConcurrencyType,运行在后台线程;defaultContext的并发类型是NSMainQueueConcurrencyType,运行在主线程,两者关系如下图:

读 MagicalRecord 源码记录_第2张图片
stack.png
  • 这里使用到了嵌套的context,当子context(这里的defaultContext)里面的managedObject数据修改了并进行保存时,子context的更改数据只会push到父context(这里的rootContext),还没保存到数据库中 ;只有当rootContext进行保存了,才能把更改数据保存到数据库中。另外,父context不会主动从子context中pull数据,除非子context进行了保存。

a)为什么要使用嵌套context呢?
在官方CoreData NSManagedObjectContext参考文档介绍到两个使用场景:1,在其他线程或队列中执行后台操作时,parentContext能处理不同线程的子context的请求;2,编辑修改数据后,这部分数据可以抛弃,不进行最后的保存,就是在子context操作修改了属于它的实体对象后不进行保存。官网文档在这里.

b)为什么嵌套的context设计为父context是privateQueueConcurrency呢,而子context为mainQueueConcurrency呢?
相对其它设计来说,这种context的设计性能一般,还凑合吧,但是容易管理多个context。
导入大量数据的时候,性能更好的当然是不使用嵌套context了,直接用privateQueue的context把数据保存到数据库,然后通过监听事件NSManagedObjectContextDidSaveNotification,在保存数据完成之后把导入的数据通过
mergeChangesFromContextDidSaveNotification的方法 merge到主线程的context,来更新主线程的context里面相关数据,设计图如下:

读 MagicalRecord 源码记录_第3张图片
stack2.png

这里涉及到Merging与Saving的区别,简单来说,子context进行save时,会将所有的数据push到父context;而merge的话,只是对context中已注册使用的对象进行更新,这样避免了对大量无关,还没有使用的对象进行更新。

更多有关context stack的设计内容,请看这篇文章吧:concurrent core data stack setup ,文章里有详细介绍分析原因还对各种设计方案进行了性能测试。


  • 在 MagicalRecord 中处理多线程:
    • 众所周知,UIKit 也是非线程安全的,我们只在主线程中操作UI,那么使用MagicalRecord的时候,我们一般使用它提供的defaultContext获取操作数据给UI显示,因为defaultContext是mainQueueConcurrencyType,运行在主线程上;

    • 那么要想在后台线程操作数据呢?那么我们可以使用+saveWithBlock:completion:方法,还提供操作完成的回调,完成的回调是在主线程,可以用来通知UI刷新数据。
      1, 在详细了解+saveWithBlock:completion:方法前,先来看看MagicalRecord以前版本提供的+MR_contextForCurrentThread方法,这个方法是获取当前线程的context,它会基于不同线程创建对应线程下的context并在该线程字典中保存context来重复使用,但这个方法将被废弃,因为返回来的context不一定正确。具体看这里,我的理解是调用该方法,返回了当前正在运行的线程对应的context,但是block继续运行不一定在同一个线程中;添加到GCD 队列的block,是有可能运行在属于GCD管理的任意的线程上,这样就造成了context不一定运行在对应的线程中。

      2,那么来看看+saveWithBlock:completion:的代码:

+ (void)saveWithBlock:(void(^)(NSManagedObjectContext *localContext))block completion:(MRSaveCompletionHandler)completion;
{
    NSManagedObjectContext *savingContext  = [NSManagedObjectContext MR_rootSavingContext];
    NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextWithParent:savingContext];

    [localContext performBlock:^{
        [localContext MR_setWorkingName:NSStringFromSelector(_cmd)];//设置context的名称便于打印区分
        if (block) {
            block(localContext);
        }
        [localContext MR_saveWithOptions:MRSaveParentContexts completion:completion];
    }];
}

创建了一个parentContext是rootContext的context,这个context是privateQueueType的,也就是拥有自己私有的线程,通过performBlock在自己私有的线程中运行block,然后使用当前context进行保存:

- (void) MR_saveWithOptions:(MRSaveOptions)saveOptions completion:(MRSaveCompletionHandler)completion;
{
    __block BOOL hasChanges = NO;
    if ([self concurrencyType] == NSConfinementConcurrencyType) {
        hasChanges = [self hasChanges];
    } else {
        [self performBlockAndWait:^{
            hasChanges = [self hasChanges];
        }];
    }
    if (!hasChanges) {//如果当前context的数据没有改动,直接主线程回调
        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(NO, nil);
            });
        }
        return;
    }
 ..................................省略代码,修改了一下代码样式否则太长了.........

    id saveBlock = ^{
        .............................................省略.............
        BOOL saveResult = NO;
        NSError *error = nil;

        @try {
            saveResult = [self save:&error];
        } @catch(NSException *exception) {
            MRLogError(@"Unable to perform save: %@", (id)[exception userInfo] ?: (id)[exception reason]);
        }  @finally  {
            [MagicalRecord handleErrors:error];
            
            if (saveResult && shouldSaveParentContexts && [self parentContext])
            { //需要保存父context的数据
                // Add/remove the synchronous save option from the mask if necessary
                MRSaveOptions modifiedOptions = saveOptions;

                if (saveSynchronously)
                {
                    modifiedOptions |= MRSaveSynchronously;
                }
                else
                {
                    modifiedOptions &= ~MRSaveSynchronously;
                }
                
                // If we're saving parent contexts, do so
                [[self parentContext] MR_saveWithOptions:modifiedOptions completion:completion];
            }
            else
            {
                if (saveResult)
                {
                    MRLogVerbose(@"→ Finished saving: %@", [self MR_description]);
                }

                if (completion)
                {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        completion(saveResult, error);
                    });
                }
            }
        }
    };

    if (saveSynchronously)  {
        [self performBlockAndWait:saveBlock];
    }  else  {
        [self performBlock:saveBlock];
    }
}

这里是根据保存参数,决定是否要同步,是否要保存parentContext,如果要保存parentContext的话,就会递归调用直到rootContext将数据保存到数据库中。
前面提到,MagicalRecord中会创建两个context,一个rootContext作为父context直接面对 Persistent Store Coordinator,一个defaultContext运行在主线程上;
那么,这里的localContext后台保存完数据后,也要同步更新defaultContext中的managedObject数据啊,这里是通过监听rootContext保存数据到数据库完成的通知NSManagedObjectContextDidSaveNotification后,在主线程合并更新相关数据到defaultContext中:

+ (void)rootContextDidSave:(NSNotification *)notification
{
    if ([notification object] != [self MR_rootSavingContext])
    {
        return;
    }
    if ([NSThread isMainThread] == NO) { //确保在主线程运行
        dispatch_async(dispatch_get_main_queue(), ^{
            [self rootContextDidSave:notification]; 
        });
        return;
    }

    for (NSManagedObject *object in [[notification userInfo] objectForKey:NSUpdatedObjectsKey]) {
        [[[self MR_defaultContext] objectWithID:[object objectID]] willAccessValueForKey:nil];
    }
    //合并更新相关数据到defaultContext中
    [[self MR_defaultContext] mergeChangesFromContextDidSaveNotification:notification];
}

这样的话,后台操作完数据,defaultContext的数据也能相应的更新,从而根据需要刷新UI。

最后


MagicalRecord中的查询等其它操作就不说了,看源码吧,也比较简单。以上均个人见解,欢迎各位小伙伴一起交流哈。

参考链接:

  • https://www.objc.io/issues/4-core-data/core-data-overview/
  • https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/index.html#//apple_ref/doc/uid/TP40001075-CH2-SW1
  • https://github.com/magicalpanda/MagicalRecord/blob/master/Docs/Working-with-Managed-Object-Contexts.md

备忘录:在调试MagicalRecord的demo时,恰巧在用SQL图形工具修改了某个表的数据还没保存,正尝试着使用MagicalRecord的后台异步保存和主线程中同时保存的测试,忽然发现demo程序卡住死锁了,也看不出哪里出了问题,然后上网各种搜索无果,重复启动demo程序继续测试也是会死锁。百思不得其解,真是我信了你的邪。

你可能感兴趣的:(读 MagicalRecord 源码记录)