Viewer 源码阅读心得2——对象存储模型

版权所有, 转载请著明出处,保留链接。
Viewer 源码阅读心得2

##对象存储模型概括
Viewer 存储模型采用CoreData, 它有两个对象,一个是对文件夹的抽象(DocumentFolder), 另一个是对PDF文件的抽象(ReaderDocument),算是比较简单的, CoreData存储文件上采用了sqlite格式,存储在Library/Application Support/Reader.sqlite。

Viewer处理存储对象模型的类在Core Data文件夹下,Core Data文件夹下有DocumentFolder, ReaderDocument, DocumentsUpdate, CoreDataManager四个类,其中DocumentFolder, ReaderDocument是模型, 它们之间是1对多关系, DocumentsUpdate, CoreDataManager…,其类层次调用关系如下:

DocumentsUpdate
    |
    |
CoreDataManager
    |
    |
DocumentFolder, ReaderDocument

##DocumentFolder类

DocumentFolder继承于NSManageObject, 是对文件夹的抽象, 其头文件如下:

@interface DocumentFolder : NSManagedObject

@property (nonatomic, strong, readwrite) NSString *name; //文件夹名称
@property (nonatomic, strong, readwrite) NSNumber *type; //文件夹类型,有用户(用户创建的),默认(Documents),最近类型(Recent)
@property (nonatomic, strong, readwrite) NSSet *documents;//存放PDF文档的集合
@property (nonatomic, assign, readwrite) BOOL isChecked;//标识文件夹是否被选中

+ (NSArray *)allInMOC:(NSManagedObjectContext *)inMOC; //返回Core Data中的所有DocumentFolder,返回数组
+ (BOOL)existsInMOC:(NSManagedObjectContext *)inMOC name:(NSString *)string; //检查指定名称的DocumentFolder是否存在,返回布尔值
+ (BOOL)existsInMOC:(NSManagedObjectContext *)inMOC type:(DocumentFolderType)kind; //检查指定类型的DocumentFolder是否存在,返回布尔值
+ (DocumentFolder *)folderInMOC:(NSManagedObjectContext *)inMOC type:(DocumentFolderType)kind;//返回指定类型的DocumentFolder对象

+ (DocumentFolder *)insertInMOC:(NSManagedObjectContext *)inMOC name:(NSString *)string type:(DocumentFolderType)kind;//插入对象
+ (void)renameInMOC:(NSManagedObjectContext *)inMOC objectID:(NSManagedObjectID *)objectID name:(NSString *)string;//重命名对象
+ (void)deleteInMOC:(NSManagedObjectContext *)inMOC objectID:(NSManagedObjectID *)objectID;//删除对象

根据Core Data, 由于DocumentFolder继承于NSManagedObject,是Core Data的模型对象,DocumentFolder(Entity 实体)的字段会自动生成存取方法,例如name属性,会自动生成:

name
setName:
primitiveName:
setPrimitiveName:

此时,如果在代码中调用这些方法,编译器会给出警告,为了编码警告,需要在类头文件中声明这些属性。DocumentFolder头文件中,还有如下一段代码:

@interface DocumentFolder (CoreDataGeneratedAccessors)

- (void)addDocumentsObject:(ReaderDocument *)value;
- (void)removeDocumentsObject:(ReaderDocument *)value;
- (void)addDocuments:(NSSet *)value;
- (void)removeDocuments:(NSSet *)value;

@end

这也是为了避免编译器警告,在头文件中增加的方法,由于documents实体字段是一对多的关系,CoreData自动生成了四个方法。

其中文件夹类型通过枚举类型定义声明如下:

typedef NS_ENUM(NSInteger, DocumentFolderType)
{
    DocumentFolderTypeUser = 0,
    DocumentFolderTypeDefault = 1,
    DocumentFolderTypeRecent = 2
};

可以看到这些方法都是类方法,分两簇,一簇是查询方法,另一簇是增、删、改方法,其中在向对象上下文(NSManagedObjectContext)增删改时,都会向通知中心发出通知,分别如下,向其它对象告诉文档发生了变化,其中对象在对象上下文中(NSManagedObjectContext)以objectID标识。

NSString *const DocumentFolderAddedNotification = @"DocumentFolderAddedNotification";
NSString *const DocumentFolderRenamedNotification = @"DocumentFolderRenamedNotification";
NSString *const DocumentFolderDeletedNotification = @"DocumentFolderDeletedNotification";

##ReaderDocument类
ReaderDocument类也继承于NSManagedObject, 是对PDF文件的抽象,其头文件如下:

@interface ReaderDocument : NSManagedObject

@property (nonatomic, strong, readwrite) NSString *guid; //PDF文件的唯一标识
@property (nonatomic, strong, readwrite) NSURL *fileURL; //PDF文件对应的URL
@property (nonatomic, strong, readwrite) NSString *fileName;//PDF文件名
@property (nonatomic, strong, readwrite) NSString *filePath;//PDF文件路径
@property (nonatomic, strong, readwrite) NSString *password;//访问PDF文件需要的密码
@property (nonatomic, strong, readwrite) NSNumber *pageCount;//PDF文件的页数
@property (nonatomic, strong, readwrite) NSNumber *pageNumber;//当前PDF文件的页码
@property (nonatomic, strong, readwrite) NSNumber *fileSize;//PDF 文件大小
@property (nonatomic, strong, readwrite) NSDate *fileDate; //PDF 文件日期
@property (nonatomic, strong, readwrite) NSDate *lastOpen;//PDF文件最后打开的日期
@property (nonatomic, strong, readwrite) NSData *tagData; //PDF tag日期
@property (nonatomic, strong, readwrite) NSManagedObject *folder; //PDF文件所属的文件夹
@property (nonatomic, strong, readonly) NSMutableIndexSet *bookmarks; //PDF文件的书签
@property (nonatomic, assign, readwrite) BOOL isChecked; //标识PDF文件是否被选中

@property (nonatomic, readonly) BOOL canEmail;//PDF文件 能通过邮件发送么
@property (nonatomic, readonly) BOOL canExport;//PDF文件 能够导出么
@property (nonatomic, readonly) BOOL canPrint;//PDF文件 能够打印么

+ (NSArray *)allInMOC:(NSManagedObjectContext *)inMOC;//返回所有的ReaderDocument对象
+ (NSArray *)allInMOC:(NSManagedObjectContext *)inMOC withName:(NSString *)name;//返回指定文件名的PDF文档
+ (NSArray *)allInMOC:(NSManagedObjectContext *)inMOC withFolder:(DocumentFolder *)object;//返回指定文件夹的PDF文档

+ (ReaderDocument *)insertInMOC:(NSManagedObjectContext *)inMOC name:(NSString *)name path:(NSString *)path;//插入对象
+ (void)renameInMOC:(NSManagedObjectContext *)inMOC object:(ReaderDocument *)object name:(NSString *)string;//重命名
+ (void)deleteInMOC:(NSManagedObjectContext *)inMOC object:(ReaderDocument *)object fm:(NSFileManager *)fm;//删除对象

+ (BOOL)existsInMOC:(NSManagedObjectContext *)inMOC name:(NSString *)string;//判断存在么

- (void)updateDocumentProperties;
- (void)archiveDocumentProperties;
- (BOOL)fileExistsAndValid;

ReaderDocument类头文件中,属性是ReaderDocument实体的字段,方法一部分是查询方法,还有一部分是增改删,当然都是通过NSManagedObjectContext走的。
这里要说一字段fileURL, ReaderDocument的实现中对fileURL, setFileURL:进行了重载,覆盖CoreData生成的方法:

- (NSURL *)fileURL
{
    [self willAccessValueForKey:@"fileURL"];
    NSURL *theURL = [self primitiveFileURL];
    [self didAccessValueForKey:@"fileURL"];
    if (theURL == nil) // Create the file URL when needed
    {
        NSString *applicationPath = [ReaderDocument applicationPath]; // Application path
        NSString *fullPath = [applicationPath stringByAppendingPathComponent:self.filePath];
        theURL = [NSURL fileURLWithPath:[fullPath stringByAppendingPathComponent:self.fileName]];
        [self setPrimitiveFileURL:theURL]; // Store the file URL for later use
    }

    return theURL;
}

- (void)setFileURL:(NSURL *)theURL
{
    [self willChangeValueForKey:@"fileURL"];
    [self setPrimitiveValue:theURL forKey:@"fileURL"];
    [self didChangeValueForKey:@"fileURL"];
}

可以看出,fileURL是applicationPath, filePath, fileName组成的,并且fileURL中调用primitiveFileURL, setFileURL:中调用setPrimitiveValue:forKey:, 因此在ReaderDocument的头文件中也对这两个方法进行了声明,避免编译器警告,如下:

@interface ReaderDocument (CoreDataPrimitiveAccessors)

- (NSURL *)primitiveFileURL;
- (void)setPrimitiveFileURL:(NSURL *)url;

@end

ReaderDocument的字段tagData存储的PDF文档的标签页数组(archived/unarchive),分别通过方法bookmarks和archiveDocumentProperties解析打包bookmarks字段。

##CoreDataManager
CoreDataManager是CoreData框架类的接口,是一个单例, 是获取NSManagedObjectModel, NSPersistentStoreCoordinator, NSManagedObjectContext类实例的接口方法的集合。头文件入下:

@interface CoreDataManager : NSObject

+ (CoreDataManager *)sharedInstance;

- (NSManagedObjectModel *)mainManagedObjectModel;
- (NSPersistentStoreCoordinator *)mainPersistentStoreCoordinator;
- (NSManagedObjectContext *)mainManagedObjectContext;
- (NSManagedObjectContext *)newManagedObjectContext;

- (void)saveMainManagedObjectContext;

@end

NSManagedObjectModel是对对象模型的抽象,主要功能是读取磁盘上的*.xcdatamodelId文件,构建对象模型关系图。因此,看一下mainManagedObjectModel的方法如下,其是在主线程中(UI线程)构建NSManagedObjectModel实例。

- (NSManagedObjectModel *)mainManagedObjectModel
{
    if (mainManagedObjectModel == nil) // Create ManagedObjectModel
    {
        assert([NSThread isMainThread] == YES); // Create it only on the main thread
        NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Reader" withExtension:@"momd"];
        mainManagedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    }

    return mainManagedObjectModel;
}

NSPersistentStoreCoordinator是数据文件管理器,抽象封装了处理底层的数据文件读取与写入,方法如下,可以看出,首先获取数据文件存放的位置,然后以NSManagedObjectModel为参数实例化一个NSPersistentStoreCoordinator,再次明确NSPersistentStoreCoordinator底层数据文件的类型及参数,这里存储文件类型为sqlite数据库文件。

- (NSPersistentStoreCoordinator *)mainPersistentStoreCoordinator
{
    if (mainPersistentStoreCoordinator == nil) // Create PersistentStoreCoordinator
    {
        assert([NSThread isMainThread] == YES); // Create it only on the main thread

        NSURL *storeURL = [CoreDataManager applicationCoreDataStoreFileURL]; // DB

        __autoreleasing NSError *error = nil; // Error information object

        NSDictionary *migrate = [NSDictionary dictionaryWithObjectsAndKeys:
                            [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                            [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];

        mainPersistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self mainManagedObjectModel]];

        if ([mainPersistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType 
                                            configuration:nil 
                                            URL:storeURL 
                                            options:migrate 
                                            error:&error] == nil)
        {
            NSLog(@"%s %@", __FUNCTION__, error); assert(NO);
        }
    }

    return mainPersistentStoreCoordinator;
}

NSManagedObjectContext封装抽象数据对象进行各种操作的全过程,并检测数据对象的变化,为CoreData对模型对象增删改查的提供接口。方法如下,可以看出是在主线程中以NSPersistentStoreCoordinator为参数实例化出一个NSManagedObjectContext实例。

- (NSManagedObjectContext *)mainManagedObjectContext
{
    if (mainManagedObjectContext == nil) // Create ManagedObjectContext
    {
        assert([NSThread isMainThread] == YES); // Create it only on the main thread
        NSPersistentStoreCoordinator *coordinator = [self mainPersistentStoreCoordinator];
        if (coordinator != nil) // Check for valid PersistentStoreCoordinator
        {
            mainManagedObjectContext = [NSManagedObjectContext new]; // New MOC

            [mainManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    return mainManagedObjectContext;
}

在NSManagedObjectContext实现文件中,对mainManagedObjectModel, mainPersistentStoreCoordinator, mainManagedObjectContext字段进行的数据的封装,保存了在主线程中创建的CoreData管理接口实例。此外newManagedObjectContext方法是以mainPersistentStoreCoordinator构建一个新的NSManagedObjectContext实例,支持CoreData中多线程操作数据用。saveMainManagedObjectContext方法,是保存有更改的数据模型对象数据到磁盘文件上。关系图如下:

NSManagedObject(模型对象)
    |
    |
NSMangedObjectContext(模型对象所在的上下文,操作数据对象接口)
    |
    |
NSPersistentStoreCoordinator(持久化存储协调者) --- NSManagedObjectModel(对象模型)
    |
    |
NSPersistentStore(硬盘持久化的目的地)

##DocumentsUpdate 类
DocumentsUpdate类是对PDF文档更新操作的封装,其实其把对PDF文档的更新操作封装在一个工作队列中,当有PDF文档需要更新时,创建一个任务operation(DocumentsUpdateOperation),放入队列,因此PDF文档是一个异步操作,不占用UI主线程。其头文件如下:

@interface DocumentsUpdate : NSObject
+ (DocumentsUpdate *)sharedInstance; //返回一个单例
+ (NSString *)documentsPath;           //返回沙盒中/documents路径
- (void)cancelAllOperations;           //取消工作队列中的任务
- (void)queueDocumentsUpdate;        //创建一个DocumentsUpdateOperation任务,加入队列,开始工作
- (BOOL)handleOpenURL:(NSURL *)theURL; //APP对外接口,URL是其他app传递过来的
extern NSString *const DocumentsUpdateOpenNotification;
@end

方法很明确,DocumentsUpdate对外封装的DocumentsUpdateOperation更新操作,DocumentsUpdateOperation对外是不可见的,是在DocumentsUpdate的实现文件中声明并实现的。而它的实例化和加入队列是在queueDocumentsUpdate方法中实现的。那么现在来看一看- (BOOL)handleOpenURL:(NSURL *)theURL方法。其他app调用本app打开此文件,会把此文件放到本app的沙盒Documents/Inbox的文件中,而传递过来的theURL参数就指向此文件位置。其逻辑过程如下:

  1. 把Documents/Inbox下的PDF文档移到Documents文件夹下,并删除Documents/Inbox下的PDF文档;
  2. 调用NSManagedObjectContext(UI主线程的), 查询该文档是否存在,如果存在,则取CoreData中的文档,如果不存在,则把传过来的PDF文档插入到CoreData中,并持久化。
  3. 设置NSUserDefaults中的当前文档键值(documentURI),并向NSNotificationCenter中心发出DocumentsUpdateOpenNotification通知(PDF文档更新打开通知)

下面来看看DocumentsUpdateOperation任务的main函数逻辑,主要都做了什么:

  1. 首先遍历Documents文件夹下的PDF文档,把他们的文件名存入fileSet数组中;
  2. 再次通过NSManagedObjectContext(工作线程的),获取所有数据库中的PDF文档,存入dataSet数组;
  3. 比较fileSet和dataSet,如果PDF文档在文件中而不再数据库中,增要增加,而如果在数据库中而不在文件中,则要删除;
  4. 把第三不的操作放在DocumentsUpdateBeganNotification,DocumentsUpdateEndedNotification通知之间,告诉其他模块这边要进行文档的更新了。

这边要强调的是,main函数中有如下方法:

[notificationCenter addObserver:self selector:@selector(handleContextDidSaveNotification:)
                        name:NSManagedObjectContextDidSaveNotification object:workMOC];

其向通知中心注册了NSManagedObjectContextDidSaveNotification通知,该通知是CoreData系统发出的,是当NSManagedObjectContext做了保存操作后发出的,其意思是当NSManagedObjectContext在其他线程中(UI主线程中)或本线程中(workMOC)做了对象模型的数据修改,并做了持久化,那么要通知其他线程中的NSManagedObjectContext(mainMOC),看以下方法handleContextDidSaveNotification:

  1. 把workMOC中的变化同步到mainMOC中(在主线程中)
  2. 把变化的对象(删除的、插入的)以参数作为通知的参数,发出DocumentsUpdateNotification,让其它对象去订阅;

对于DocumentsUpdate类,对外接口只有一个queueDocumentsUpdate,即更新文档,那么在什么时候调用呢…,请看下节Viewer源码阅读心得3——初始化分析。
版权所有, 转载请著明出处,保留链接。