IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Magical Record 全面解析

    shendao发表于 2017-06-02 05:36:40
    love 0

    Magical Record是用来操作Core Data的一个第三方工具,在介绍Magical Record 之前必须要先了解一下Core Data的基本概念

    Core Data基本介绍

    Core Data Stack

    核心数据堆栈是由一个或多个与单个persistent store coordinator关联的managed object contexts 组成,而persistent store coordinator是和一个或多个persistent stores关联在一起。堆栈包含了CoreData的所有组件查询,创建,操作managed objects.
    简单来说包含了:

    • 一个包含了记录的persistent store.类似于数据库
    • 一个在本地数据和对象之间的persistent object store
    • 一个聚合了所有存储的persistent store coordinator
    • 一个描述实体的managed object model
    • 一个容器包含managed objects的managed object context容器

    可能有点绕,不过一看图世界就清晰了

    如下图:

    Magical Record 全面解析

    Managed Object

    Managed Object是一个模型对象(模型-视图-控制器的意义上),它代表了一个持久存储的记录。管理对象是实例NSManagedObject或子类NSManagedObject。

    管理对象有一个实体的描述对象,告诉它代表着什么实体的引用。以这种方式,NSManagedObject可以表示任何实体不需要每个实体的唯一的子类。如果要实现自定义行为,例如计算派生属性值,或者为了实现验证逻辑可以使用一个子类。

    还是来看图:

    Magical Record 全面解析

    Managed Object Model

    Magical Record 全面解析

    Manage Context Object

    Manage Context Object代表单个对象的空间,,在核心数据的应用程序。管理对象上下文的一个实例的NSManagedObjectContext。它的主要职责是管理管理对象的集合。这些管理对象代表一个或多个持久存储的一个内部一致的看法。上下文是在管理对象的生命周期核心作用。

    上下文是在核心数据堆栈中的中心对象。这是你用它来创建和获取管理对象和管理撤消和恢复操作的对象。内的给定范围内,有至多一个被管理目标代表在永久存储器的任何给定的记录。

    Magical Record 全面解析

    上下文被连接到一个父对象存储。这通常是一个持久存储协调,但可能是另一个管理对象上下文。当你获取对象,上下文要求其父对象存储返回那些符合提取请求的对象。您对管理对象的修改,直到您保存的背景下不被提交到父store。

    在某些应用中,你可能想保持独立组来管理对象和编辑这些对象的; 或者你可能需要执行使用一个上下文,同时允许用户与另一个对象交互的后台操作

    Persistent Store Coordinator

    哎!翻译太累了。直接上图吧

    Magical Record 全面解析

    这张图把这个的架构解释得非常清楚

    Fetch Request

    Magical Record 全面解析

    官方文档

    开始使用Magical Record

    导入MagicalRecord.h在项目的预编译文件*.pch中。这保证了可以全局访问所需要的头文件。

    使用了CocoaPods或者MagicalRecord.framework,用如下方式导入:

    // Objective-C #import <MagicalRecord/MagicalRecord.h> // Swift import MagicalRecord

    如果是把源文件直接放到项目中,则直接#import "MagicalRecord.h"

    接下里,在app delegate的某些地方,比如- applicationDidFinishLaunching: withOptions:或者-awakeFromNib,使用下面的某一个方法来配置MagicalRecord.

    + (void)setupCoreDataStack; + (void)setupAutoMigratingCoreDataStack; + (void)setupCoreDataStackWithInMemoryStore; + (void)setupCoreDataStackWithStoreNamed:(NSString *)storeName; + (void)setupCoreDataStackWithAutoMigratingSqliteStoreNamed:(NSString *)storeName; + (void)setupCoreDataStackWithStoreAtURL:(NSURL *)storeURL; + (void)setupCoreDataStackWithAutoMigratingSqliteStoreAtURL:(NSURL *)storeURL;

    每次调用Core Data的堆栈的实例,提供给了这些实例的getter,setter方法。这些实例被MagicalRecord很好的管理,被识别为默认方式。

    当通过DEBUG模式标识使用SQLite数据库,不创建新的model版本来改变model将会引起MagicalRecord自动的删除老的数据库并且自动的创建一个新的。这样可以节约很多时间–不需要每次都卸载重装app来让data model改变,确保你的app不是用的DEBUG模式:当删除app数据的时候不告诉用户真的是一种很糟糕的方式

    在你的app退出之前,你应该调用类方法+cleanUp

    [MagicalRecord cleanUp];

    这将会清理MagicalRecord,比如自定义的错误处理,让通过MagicalRecord创建的Core Data堆栈为nil.

    使用Managed Object Contexts

    创建新的上下文

    一些简单的类方法用来帮助快速的你创建新的上下文

      • [NSManagedObjectContext MR_newMainQueueContext]:
      • [NSManagedObjectContext MR_newPrivateQueueContext]:
      • [NSManagedObjectContext MR_newContextWithStoreCoordinator:…]: 允许你具体化persistent store coordinator为新的上下文,有一个NSPrivateQueueConcurrencyType

    默认上下文

    当使用CoreData,你将不断的和两个主要的对象打交道,NSManagedObject 和 NSManagedObjectContext.

    MagicalRecord提高了一个简单的类方法来获取默认的NSManagedObjectContext,这个上下文贯穿了你的app始终,这个上下文的操作会在在主线程中进行,并且对于单线程的app比较适合。

    通过如下方式访问到默认的上下文:

    NSManagedObjectContext *defaultContext = [NSManagedObjectContext MR_defaultContext];

    这个上下文将在MagicalRecord任何使用了上下文的方法中使用,但是没有提供一个具体的NSManagedObjectContext参数。

    如果你需要创建一个不再主线程中使员工的上下文,使用:

    NSManagedObjectContext *myNewContext = [NSManagedObjectContext MR_newContext];

    这种方式将会创建一个和default context有相同的对象和persistent store.安全在其他线程使用。它将会默认的将default context作为它的父上下文。

    如果你想默认让myNewContext实例化所有的fetch request.使用类方法的方式

    [NSManagedObjectContext MR_setDefaultContext:myNewContext];

    注意:高度建议default context使用类型为NSMainQueueConcurrencyType的上下文来创建并设置在主线程。

    在后台线程中执行

    MagicalRecord提供了方法来设置,协调上下文在后台线程中使用。后台保存操作受到了UIView动画使用Block的方式,但也存在了一些不同

    • 在你对实体进行改变了的block,绝对不在主线程中执行

    • 单个的NSManagedObjectContext提供了block使用。

    举个例子,你有一个Person实体,并且需要设置firstName和lastName,下面的代码展示了你怎样通过MagicalRecord来设置后台上下文进行使用。

    Person *person = ...;  [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){    Person *localPerson = [person MR_inContext:localContext];   localPerson.firstName = @"John";   localPerson.lastName = @"Appleseed";  }];

    在这个方法,具体的block提供了一个合适的上下文让你进行操作,不需要担心去设置上下文,以便它告诉default context已经做了。并且应该更新,因为是在其他线程里面改变进行的。

    当执行完了saveBlock,你可以在completion block做些操作

    Person *person = ...;  [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){    Person *localPerson = [person MR_inContext:localContext];   localPerson.firstName = @"John";   localPerson.lastName = @"Appleseed";  } completion:^(BOOL success, NSError *error) {    self.everyoneInTheDepartment = [Person findAll];  }];

    completion block在主线程中被调用,为了UI更新更安全。

    创建实体

    在默认的上下文中插入一个实体,如下:

    Person *myPerson = [Person MR_createEntity];

    在具体的上下文中插入一个实体
    Person *myPerson = [Person MR_createEntityInContext:otherContext];

    删除一个实体

    在默认上下文中删除:
    [myPerson MR_deleteEntity];

    在具体上下文中删除:
    [myPerson MR_deleteEntityInContext:otherContext];

    截断所有实体在默认上下文
    [Person MR_truncateAll];

    截断所有实体在具体上下文
    [Person MR_truncateAllInContext:otherContext];

    查询实体

    基本查找

    在MagicalRecord大多数方法是返回一个NSArray数组。

    举例,如果你有一个person实体和department实体关联,你可以查询所有的person实体从persistent store通过如下方式实现:

    NSArray *people = [Person MR_findAll];

    传入一个具体的参数返回一个排序后的数组:

    NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName" ascending:YES];

    传入多个具体的参数返回一个排序后的数组

    NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName,FirstName" ascending:YES];

    传入多个不同参数值得到排序结果,如果你不提供任何一个参数的默认值,就会默认使用你在model中的设置。

    NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName:NO,FirstName"                                          ascending:YES];  // OR  NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName,FirstName:YES"                                          ascending:NO];

    如果你有一种唯一从数据库中查询单个对象的方法(比如作为唯一属性),你可以通过下面的方法:

    Person *person = [Person MR_findFirstByAttribute:@"FirstName" withValue:@"Forrest"];

    高级查找

    如果想去具体化你的搜索,你可以使用谓词

    NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", @[dept1, dept2]]; NSArray *people = [Person MR_findAllWithPredicate:peopleFilter];

    返回NSFetchRequest

    NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", departments]; NSFetchRequest *people = [Person MR_requestAllWithPredicate:peopleFilter];

    关于每一行的调用, NSFetchRequest 和 NSSortDescriptor作为排序的标配。

    自定有Requset

    Predicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", departments];  NSFetchRequest *peopleRequest = [Person MR_requestAllWithPredicate:peopleFilter]; [peopleRequest setReturnsDistinctResults:NO]; [peopleRequest setReturnPropertiesNamed:@[@"FirstName", @"LastName"]];  NSArray *people = [Person MR_executeFetchRequest:peopleRequest];

    查询实体数量

    可以执行所有实体类型输血量在persistent store

    NSNumber *count = [Person MR_numberOfEntities];

    或者基于查询的数量
    NSNumber *count = [Person MR_numberOfEntitiesWithPredicate:...];

    这里有一组方法来返回NSUInteger而不是NSNumber

    + (NSUInteger) MR_countOfEntities; + (NSUInteger) MR_countOfEntitiesWithContext:(NSManagedObjectContext *)context; + (NSUInteger) MR_countOfEntitiesWithPredicate:(NSPredicate *)searchFilter; + (NSUInteger) MR_countOfEntitiesWithPredicate:(NSPredicate *)searchFilter                                       inContext:(NSManagedObjectContext *)context;

    聚合操作

    NSNumber *totalCalories = [CTFoodDiaryEntry MR_aggregateOperation:@"sum:"                                                        onAttribute:@"calories"                                                      withPredicate:predicate];  NSNumber *mostCalories  = [CTFoodDiaryEntry MR_aggregateOperation:@"max:"                                                        onAttribute:@"calories"                                                      withPredicate:predicate];  NSArray *caloriesByMonth = [CTFoodDiaryEntry MR_aggregateOperation:@"sum:"                                                         onAttribute:@"calories"                                                       withPredicate:predicate                                                            groupBy:@"month"];

    在具体的上下文中查找实体

    所有的 find, fetch, request方法都有一个inContext:,方法参数允许具体使用哪一个上下文查询:

    NSArray *peopleFromAnotherContext = [Person MR_findAllInContext:someOtherContext];  Person *personFromContext = [Person MR_findFirstByAttribute:@"lastName"                                                    withValue:@"Gump"                                                    inContext:someOtherContext];  NSUInteger count = [Person MR_numberOfEntitiesWithContext:someOtherContext];

    存储实体

    什么时候应该保存

    总的来说,当数据发生改变的时候应该保存到persistent store(s).一些应用选择在应用终止的时候才保存。然而,在大多数场景下是不需要的。事实上,只在应用终止的时候保存,会有数据丢失的风险。万一你的应用崩溃了怎么办?用户将丢失他对数据所做的改变。那样的话是一种相当糟糕的体验,可以简单的避免。

    如果你觉得执行保存话费了大量的时间,有几件事情需要考虑:

    • 1.在后台线程保存:MagicalRecord提高了非常简单的API来让改变的实例按顺序的在后台保存,举个例子:
    [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {      // Do your work to be saved here, against the `localContext` instance     // Everything you do in this block will occur on a background thread  } completion:^(BOOL success, NSError *error) {     [application endBackgroundTask:bgTask];     bgTask = UIBackgroundTaskInvalid; }];
    • 2.将任务分解为多个小任务保存:比如大量数据的导入应被分解为小块,没有确切的原则来决定一次导入多少数据,你需要测量你应用的性能比如通过苹果的Instruments和tune.

    解决长期运行中的保存

    在iOS上

    当应用在iOS上终止运行,有一个很小的机会去清理,保存数据到磁盘。如果你知道保存操作可能会花一段时间,最好的方式就是去申请一个额外的截止时间。比如:

    UIApplication *application = [UIApplication sharedApplication];  __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{     [application endBackgroundTask:bgTask];     bgTask = UIBackgroundTaskInvalid; }];  [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {      // Do your work to be saved here  } completion:^(BOOL success, NSError *error) {     [application endBackgroundTask:bgTask];     bgTask = UIBackgroundTaskInvalid; }];

    确保仔细的读过[

    • beginBackgroundTaskWithExpirationHandler:](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplication_Class/index.html#//apple_ref/occ/instm/UIApplication/beginBackgroundTaskWithExpirationHandler:),不合适或者不需要的延长应用的存活时间可能让你的应用被拒。

    使用模式

    • 保存数据
    • 查找实体
    • 导入数据
    • 线程安全

    日志

    MagicalRecord建立了在和Core Data交互的时候的日志。当错误发生的时候,这些错误将会被捕获。并且将打印到控制台。

    日志被配置为输出调试信息(MagicalRecordLoggingLevelDebug)在deug编译的时候默认的,将会输出日志信息MagicalRecordLoggingLevelError在realease下。

    日志通过[MagicalRecord setLoggingLevel:]配置,使用下面的几种预定义的日主等级。

    • MagicalRecordLogLevelOff:不开始日志
    • MagicalRecordLoggingLevelError:记录所有错误
    • MagicalRecordLoggingLevelWarn:记录警告和错误
    • MagicalRecordLoggingLevelInfo:记录日志有用的信息,错误,警告
    • MagicalRecordLoggingLevelDebug:所有调试信息
    • MagicalRecordLoggingLevelVerbose:日志冗长的诊断信息,有用的信息,错误,警告

    日志默认等级是 MagicalRecordLoggingLevelWarn

    关闭日志

    大多数人而言,这个不需要,设置日志等级为MagicalRecordLogLevelOff将会保证不再打印日志信息

    甚至当使用了MagicalRecordLogLevelOff,快速检测检查可能被调用无论何时日志被调用。如果想绝对的关闭日志。你可以定义如下,当编译MagicalRecord的时候
    #define MR_LOGGING_DISABLED 1

    请注意:这个之后再增加源码到项目中才会起作用。你也可以增加MagicalRecord项目的OTHER_CFLAGS为-DMR_LOGGING_DISABLED=1

    日志在2.3.0版本有问题,不能正常的显示到控制器

    google到了解决的方法副在下面

    For the development branch (version 2.3.0 and higher) of Magical Record logging seems to still not work correctly. When imported like this: pod 'MagicalRecord', :git => 'https://github.com/magicalpanda/MagicalRecord', :branch => 'develop'  I have no logging output on my Xcode console. But I altered the post_install script of the Cocoapod. The following should enable logging: https://gist.github.com/Blackjacx/e5f3d62d611ce435775e  With that buildsetting included in GCC_PREPROCESSOR_DEFINITIONS logging of Magical Record can be controlled in 2.3.0++ by using [MagicalRecord setLoggingLevel:]
    • 脚本:
    post_install do |installer|     installer.project.targets.each do |target|         target.build_configurations.each do |config|             # Enable the loggin for MagicalRecord             # https://github.com/magicalpanda/MagicalRecord/wiki/Logging             if target.name.include? "MagicalRecord"                 preprocessorMacros = config.build_settings['GCC_PREPROCESSOR_DEFINITIONS']                 if preprocessorMacros.nil?                     preprocessorMacros = ["COCOAPODS=1"];                 end                 preprocessorMacros << "MR_LOGGING_ENABLED=1" config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = preprocessorMacros             end         end     end end

    自己尝试遇到的坑

    日记记录

    2.3.0版本同样遇到了日志不能正常输出到控制台的问题,虽然能够拿到解决问题的脚步,但是自己在taget,buildsetting里面都设置了还是没有用。自己对cocopods管理的原理还不是很明白。

    上下文的坑

    NSManagedObjectContext这个类是CoreData里面非常重要的类。它有父上下文和子上下文的概念。经过了漫长的爬坑,终于在苹果官方文档中找到了关于它详细的介绍。

    这里只截取parent store这节来讲

    Managed object contexts有一个父存储,通过它来检索数据,提交改变

    最开始在iOS5的之前,父存储一直是persistent store coordinator。在iOS5之后。父存储的类型可以是其他的Managed object contexts。但是最终的根context必须是`persistent store coordinator“。协调者提高被管理的对象模型,调用各种对数据库的请求。

    如果父存储是一个Managed object contexts。查询,保存的操作是被父存储来协调的而不是persistent store coordinator。这种方式有两个好处,

    • 1.在其他线程中执行操作
    • 2.管理废弃的编辑,比如监视窗口、view
      第一种场景,父上下文能够通过不同的线程从子中获得请求,

    • 重点部分:当在上下文中保存所做的改变的时候,改变只会被提交一次存储,如果有子的上下文,改变将会推到他的父上下文,改变不会直接保存到数据库,直到根上下文被保存才会保存到数据库(根管理对象的上下文的父上下文为空)。除此之外,父上下文在保存之前不会从子中拉取数据的改变。如果你想最后提交数据的改变,必须保存子上下文,这样就可以推到父上下文中。

    测试代码

    上下文的创建时通过线程来控制,也就是上下文和线程相关。[[NSThread currentThread] threadDictionary];返回的字典就是处理数据方面的。

    if ([NSThread isMainThread])     {         return [self MR_defaultContext];     }     else     {         int32_t targetCacheVersionForContext = contextsCacheVersion;            NSMutableDictionary *threadDict = [[NSThread currentThread] threadDictionary];         NSManagedObjectContext *threadContext = [threadDict objectForKey:kMagicalRecordManagedObjectContextKey];         NSNumber *currentCacheVersionForContext = [threadDict objectForKey:kMagicalRecordManagedObjectContextCacheVersionKey];          // 保证两者同时存在,或者同时不存在         NSAssert((threadContext && currentCacheVersionForContext) || (!threadContext && !currentCacheVersionForContext),                  @"The Magical Record keys should either both be present or neither be present, otherwise we're in an inconsistent state!");         // 不存在上下文         if ((threadContext == nil) || (currentCacheVersionForContext == nil) || ((int32_t)[currentCacheVersionForContext integerValue] != targetCacheVersionForContext))         {             // 创建新的上下文             threadContext = [self MR_contextWithParent:[NSManagedObjectContext MR_defaultContext]];             [threadDict setObject:threadContext forKey:kMagicalRecordManagedObjectContextKey];             [threadDict setObject:[NSNumber numberWithInteger:targetCacheVersionForContext]                            forKey:kMagicalRecordManagedObjectContextCacheVersionKey];         }         return threadContext;     }

    在配置的时候就会默认创建两种上下文,一个根上下文,和协调者直接通信的,一个是主线程相关的默认上下文。默认上下文是根上下文的子。

    • 有必要说一说MR_saveWithBlock这个方法,自己在写的时候就犯错了。

    开看看实现

    - (void)MR_saveWithBlock:(void (^)(NSManagedObjectContext *localContext))block completion:(MRSaveCompletionHandler)completion; {     NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextWithParent:self];      [localContext performBlock:^{         [localContext MR_setWorkingName:NSStringFromSelector(_cmd)];          if (block) {             block(localContext);         }          [localContext MR_saveWithOptions:MRSaveParentContexts completion:completion];     }]; }

    是在当前的上下文中新建子然后通过子去保存,注意这里的保存方法有个参数MRSaveParentContexts,会连同父上下文一起通常,

    在保存的方法中有一段:

     // 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];

    类似于递归调用,最终会调用根上下文,也就是保存到了数据库。

    但是在这之前有个逻辑想到重要。也就是保存的上下文该没有改变。如果被确定是没有改变的,那就不会中保存的逻辑。

         __block BOOL hasChanges = NO;      if ([self concurrencyType] == NSConfinementConcurrencyType)     {         hasChanges = [self hasChanges];     }     else     {         [self performBlockAndWait:^{             hasChanges = [self hasChanges];         }];     }      if (!hasChanges)     {         MRLogVerbose(@"NO CHANGES IN ** %@ ** CONTEXT - NOT SAVING", [self MR_workingName]);          if (completion)         {             dispatch_async(dispatch_get_main_queue(), ^{                 completion(NO, nil);             });         }          return;     }

    最后来一段有问题的代码。

         // 在默认的上下文中创建实体     Person *person = [Person MR_createEntity];     // 改变person,引起上下文的改变     person.name = @"test";     person.age = @(100);     [[NSManagedObjectContext MR_defaultContext] MR_saveWithBlock:^(NSManagedObjectContext * _Nonnull localContext) {      } completion:^(BOOL contextDidSave, NSError * _Nullable error) {      }];

    这段代码不会保存成功。

    因为在MR_saveWithBlock创建一个继承自上下文的心的localContext。然而person所做的改变是在默认上下文中,也即是localContext的父上下文。判断是否改变是根据localContext来判断的,结果就是hasChanges为NO。最终导致保存不成功。

    那么改变一下就可以了。也即是我们自己来控制保存。
    如下:

        // 在默认的上下文中创建实体     Person *person = [Person MR_createEntity];     // 改变person,引起上下文的改变     person.name = @"test";     person.age = @(100);     [[NSManagedObjectContext MR_defaultContext] MR_saveWithOptions:MRSaveParentContexts                                                         completion:^(BOOL contextDidSave, NSError * _Nullable error) {      }];

    总结:

    多看官方文档,多看三方库wiki,多总结。

    养成有耐心的习惯。勿急躁。



沪ICP备19023445号-2号
友情链接