读 MagicalRecord 源码记录

作者: 一剑书生 | 来源:发表于2016-04-19 13:16 被阅读723次

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

    MagicalRecord 的好处


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

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


    • 先来回顾一下CoreData相关的概念与对象,不多说,请看图:
    来源于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,运行在主线程,两者关系如下图:

    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里面相关数据,设计图如下:

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

    参考链接:


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

    相关文章

      网友评论

      • 李Mr:请问你文章末所提到的bug 找到了吗,我最近也遇到这样的问题。不知道问题出在哪里?
        一剑书生:哈哈,那是因为SQL图形工具锁住了正在操作的表!
      • wg689:不错
      • 牧童s:楼主好,请教个问题,我现在有两个实体,一个user,一个friends,他们建立关系,一个user对应多个friends,我通过user 查询所有friends 是空啊,但是数据库是有数据的
        牧童s:@一剑书生 Model
        import Foundation
        import CoreData
        @ObjC (UserModel)
        class UserModel: NSManagedObject {
        // Insert code here to add functionality to your managed object subclass
        @NSManaged var userId: String?
        @NSManaged var friends: NSSet?
        }
        @ObjC (FriendModel)
        class FriendModel: NSManagedObject {

        // Insert code here to add functionality to your managed object subclass
        @NSManaged var name: String?
        @NSManaged var image: String?
        @NSManaged var birth: String?
        @NSManaged var uid: String?
        @NSManaged var user: UserModel?
        }
        这里的friends怎么是NSSet类型呢,我用editor直接生成的Model
        牧童s:@一剑书生 直接看代码了: let user = UserModel.MR_findByAttribute("userId", withValue:userId)
        if user?.count > 0 {
        let currentUser = user?.first as! UserModel
        let friends = FriendModel.MR_findByAttribute("user", withValue: currentUser)
        print(friends)
        }
        一剑书生:具体是怎么查的?

      本文标题:读 MagicalRecord 源码记录

      本文链接:https://www.haomeiwen.com/subject/epmolttx.html