读 MagicalRecord 源码记录
MagicalRecord 这个库,用过CoreData的人都应该听说过它吧。有人说 CoreData 巨坑,有人说是坑也得跳。但是,用上了 MagicalRecord 之后,也许你能躺着过坑。既然项目用到了CoreData,那就来看下MagicalRecord的源码吧,我阅读的版本是2.3.2。
MagicalRecord 的好处
1, 清理你的CoreData相关代码,即它帮你省掉一大部分CoreData的代码编写
2, 简单清晰,一行代码就可以查询数据
3, 当需要优化查询数据的时候,可以对NSFetchRequest进行修改
MagicalRecord 怎么帮我们省掉CodeData的代码呢?
- 先来回顾一下CoreData相关的概念与对象,不多说,请看图:
- 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
,运行在主线程,两者关系如下图:
- 这里使用到了嵌套的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里面相关数据,设计图如下:
这里涉及到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程序继续测试也是会死锁。百思不得其解,真是我信了你的邪😂。