来源:伯乐在线
凭良心讲,我不能告诉你不去使用Core Data。它不错,而且也在变好,并且它被很多其他Cocoa开发者所理解,当有新人加入你的组或者需要别人接手你的项目的时候,这点很重要。
更重要的是,不值得花时间和精力去写自己的系统去代替它。真的,使用Core Data吧。
为什么我不使用Core Data
Mike Ash写到:就我自己而言,我不是个狂热粉丝。我发现API是笨拙的,并且框架本身对于大量的数据是极其缓慢的。 |
一个实际的例子:10,000条目
大部分情况下这样没关系。但是设想那个Feed里有200个文章,为了避免阻塞主线程,你可能考虑在后台线程里做这个工作(特别当你的程序是一个iPhone应用)。当你一开始使用Core Data多线程,事情就开始变的不好处理了。
这可能还凑合,至少不值得切换走Core Data。
但是接下来加同步。
另一个例子:快速启动
我想减少我的另一个程序的启动时间,不只是开始的时间,而是在数据显示之前的所有时间。
关于iPhone的应用(或者所有应用)我的理论是,启动时间很重要,比其他大部分开发者想的都要重要。应用的启动很慢看起来不像是要启动一样,因为人们潜意识里记得,并且会产生阻止启动应用的想法。减少启动时间就减少了摩擦,让用户更有可能继续使用你的应用,并且推荐给其他人。这是你让你的应用成功的一部分。
因为我不使用Core Data,我手边有一个简单的,保守的解决方案。我把timeline(消息和人物对象)通过NSCoding保存到一个plist文件中。启动的时候它读这个文件,创建消息和人物对象,UI一出现就显示时间轴。
这明显的减少了延迟。
在更新更快的机器出来后,我去掉了那些代码。回顾过去,我希望我可以把它留下来。
我怎么考虑这个问题
当考虑是否使用Core Data时,我考虑下面这些事情:
会有难以置信数量的数据吗?
会有一个Web API包含类似于数据库的终端吗(对比类对象终端)?
用户可能通过操作处理大量对象吗?
当我决定使用Core Data(我已经发布过使用Core Data的应用),我会小心留意我怎么使用它。为了得到好的性能,我发现我把它当做一个sql数据库的一个奇怪接口来使用,然后我知道我应该舍弃Core Data,直接使用sqlite。
我怎么使用sqlite
我通过FMDB Wrapper来使用sqlite,FMDB来自Flying Meat Software,由Gus Mueller提供。
基本操作
我在iPhone以前,Core Data以前就使用过sqlite。这是它怎么工作的的要点:
2. 我大量使用blocks来让异步程序容易点。
3. 模型对象只存在在主线程(但有两个重要的例外),改变会触发一个后台保存。
5,一些模型对象是唯一的,一些不是。取决于应用的需要(大部分情况是唯一的)。
6. 对关系型数据,我尽可能避免连表查询。
7. 一些对象类型在启动的时候就完全读入内存,另一些对象类型可能只需要创建并维护一个他们的uniqueIDs的。NSMutableSet,所以不需要去触及数据库,我就知道已经有什么。
我会通过我现在的应用的代码来详细描述。
数据库更新
- -[VSDatabaseControllerrunDatabaseBlockInTransaction:(VSDatabaseUpdateBlock)databaseBlock]
VSDatabaseUpdateBlock很简单:
- typedefvoid(^VSDatabaseUpdateBlock)(FMDatabase*database);
runDatabaseBlockInTransaction也很简单:
- -(void)runDatabaseBlockInTransaction:(VSDatabaseUpdateBlock)databaseBlock{
- dispatch_async(self.serialDispatchQueue,^{
- @autoreleasepool{
- [selfbeginTransaction];
- databaseBlock(self.database);
- [selfendTransaction];
- }
- });
- }
(注意我用自己的连续调度队列。Gus建议看一下FMDatabaseQueue,也是一个连续调度队列。我还没能去看一下,因为它比FMDB的其他东西都要新。)
beginTransaction和endTransaction的调用是可嵌套的(在我的数据库控制器里)。在合适的时候他们会调用-[FMDatabase beginTransaction] 和 -[FMDatabase commit]。(使用事务是让sqlite变快的关键。)提示:我把当前事务存储在-[NSThread threadDictionary]。它很好获取每一个线程的数据,我几乎从不用其他的。
- -(void)emptyTagsLookupTableForNote:(VSNote*)note{
- NSString*uniqueID=note.uniqueID;
- [selfrunDatabaseBlockInTransaction:^(FMDatabase*database){
- [databaseexecuteUpdate:
- @"deletefromtagsNotesLookupwherenoteUniqueID=?;",uniqueID];
- }];
- }
像VSDatabaseController的所有其他公共接口,emptyTagsLookupTableForNote应该在主线程中被调用。模型对象只能在主线程中被引用,所以在block中用uniqueID,而不是VSNote对象。
注意在这种情况下,我更新了一个查找表。Notes和tags是多对多关系,一种表现方式是用一个数据库表映射note uniqueIDs和tag uniqueIDs。这些表不会很难维护,但是如果可能,我确实尝试避免他们的使用。
注意在更新字符串中的?。-[FMDatabase executeUpdate:] 是一个可变参数函数。sqlite支持使用占位符?,所以你不需要把正真的值放入字符串。这儿有一个安全问题:它帮助守护程序反对sql插入。如果你需要避开某些值,它也为你省了麻烦。
- [self.databaseexecuteUpdate:
- @"CREATEINDEXifnotexistsnoteUniqueIDIndexontagsNotesLookup(noteUniqueID);"];
- -[VSDatabaseControllerrunFetchForClass:(Class)databaSEObjectClass
- fetchBlock:(VSDatabaseFetchBlock)fetchBlock
- fetchResultsBlock:(VSDatabaseFetchResultsBlock)fetchResultsBlock];
这两行代码做了大部分工作:
用FMDB查找数据库返回一个FMResultSet. 通过resultSet你可以逐句循环,创建模型对象。
我建议写通用的代码去转换数据库行到对象。一种我使用的方法是用一个plist,映射column名字到对象属性。它也包含类型,所以你知道是否需要调用 -[FMResultSet dateForColumn:], -[FMResultSet stringForColumn:]或其他。
唯一对象
通常我有uniqued对象。同一个数据库行结果始终对应同一个对象。
为了做到唯一,我创建了一个对象缓存,一个NSMapTable,在init函数里:_objectCache = [NSMapTable weakToWeakObjectsMapTable]。我来解释一下:
如果你的对象缓存是一个NSMutableDictionary,你将需要做一些额外的工作来清空缓存中的对象。确定它对应的对象在别的地方是否有引用就变的很痛苦。NSMapTable是弱引用,就会自动处理这个问题。
所以:我们在主线程中让对象唯一。如果一个对象已经在对象缓存中存在,我们就用那个存在的对象。(主线程胜出,因为它可能有新的改变。)如果对象缓存中没有,它会被加上。
保持对象在内存中
有很多次,把整个对象类型保留在内存中是有道理的。我最新的app有一个VSTag对象。虽然可能有成百上千个笔记,但tags的数量很小,基本少于10。一个tag只有6个属性:3个BOOL,两个很小的NSstring,还有一个NSDate。
启动的时候,app获取所有tags并且把他们保存在两个字典里,一个主键是tag的uniqueID,另一个主键是tag名字的小写。
但是很多次,把所有数据保留在内存中是不实际的。比如我们不会在内存中保留所有笔记。
但是也有很多次,当不能在内存中保留对象时,你希望在内存中保留所有uniqueIDs。你会像这样做一个获取:
- FMResultSet*resultSet=[self.databaseexecuteQuery:@"selectuniqueIDfromsome_table"];
resultSet只包含了uniqueIDs, 你可以存储到一个NSMutableSet里。
我发现有时这个对web APIs很有用。想象一个API调用返回从某个确定的时间以后的,已创建笔记的uniqueIDs列表。如果我本地已经有了一个包含所有笔记uniqueIDs的NSMutableSet,我可以快速检查(通过 -[NSMutableSet minusSet])是否有漏掉的笔记,然后去调用另一个API下载那些漏掉的笔记。这些完全不需要触及数据库。
但是,像这样的事情应该小心处理。app可以提供足够的内存吗?它真的简化编程并且提高性能了吗?
Web APIs
是这样的:一个数据库对象有一个detachedCopy方法,可以复制数据库对象。这个复制对象不是引用自我用来唯一化的对象缓存。唯一引用那个对象的地方是API调用,当API调用结束,那个复制的对象就消失了。
- -(void)uploadNote:(VSNote*)note{
- VSNoteAPICall*apiCall=[[VSNoteAPICallalloc]initWithNote:[notedetachedCopy]];
- [selfenqueueAPICall:apiCall];
- }
VSNoteAPICall从复制的VSNote获取值,并且创建HTTP请求,而不是一个字典或其他笔记的表现形式。
处理Web API返回值
我对web返回值做了一些类似的事情。我会对返回的JSON或者XML创建一个模型对象,这个模型对象也是分离的。它不是存储在为了唯一性的模型缓存里。
但是可能那个对象有一个在内存中的版本,幸运的是我们很容易找到:
- VSNote*cachedNote=[self.mapTableobjectForKey:downloadedNote.uniqueID];
一旦cachedNote更新了,观察者会通过KVO通知笔记,或者我会发送一个NSNotification,或者两者都做。
Web API调用也会返回一些其他值。我提到过RSS阅读器可能获得一个已读条目的大列表。这种情况下,我用那个列表创建了一个NSSet,在内存中更新每一个缓存文章的read属性,然后调用-[FMDatabase executeUpdate:]。
数据库迁移
比如加一个表:
- [self.databaseexecuteUpdate:@"CREATETABLEifnotexiststags"
- "(uniqueIDTEXTUNIQUE,nameTEXT,deletedINTEGER,deletedModificationDateDATE);"];
或者加一个索引:
- [self.databaseexecuteUpdate:@"CREATEINDEXifnotexists"
- "archivedSortDateIndexonnotes(archived,sortDate);"];
或者加一列:
- [self.databaseexecuteUpdate:@"ALTERTABLEtagsADDdeletedDateDATE"];
性能技巧
sqlite可以非常非常快,它也可以非常慢。完全取决于你怎么使用它。
事务
如果你不得不反规范化( Denormalize)
我总是疯狂避免它,直到这样能有严重的性能区别。然后我会尽可能少得这么做。
使用索引
我的应用中tags表的创建语句像这样:
- CREATETABLEifnotexiststags
- (uniqueIDTEXTUNIQUE,deletedModificationDateDATE);
- CREATEINDEXifnotexiststagNameIndexontags(name);
你可以一次性在多列上创建索引,像这样:
- CREATEINDEXifnotexistsarchivedSortDateIndexonnotes(archived,sortDate);
但是注意太多索引会降低你的插入速度。你只需要足够数量并且是对的那些。
使用命令行应用
打开以后,你可以看schema: type .schema。
真实的例子
然后我可以执行一个查询:
- selectdistincttagUniqueIDfromtagsNotesLookupwherearchived=0;
我已经有了一个在tagUniqueID上的索引。所以我用explain query plan来告诉我当我执行这个查询的时候会发生什么。
- sqlite>explainqueryplanselectdistincttagUniqueIDfromtagsNotesLookupwherearchived=0;
- 0|0|0|SCANTABLEtagsNotesLookupUSINGINDEXtagUniqueIDIndex(~100000rows)
我在tagUniqueID和archive上建了索引:
- CREATEINDEXarchivedTagUniqueIDontagsNotesLookup(archived,tagUniqueID);
再次执行explain query plan:
- sqlite>explainqueryplanselectdistincttagUniqueIDfromtagsNotesLookupwherearchived=0;
- 0|0|0|SEARCHTABLEtagsNotesLookupUSINGCOVERINGINDEXarchivedTagUniqueID(archived=?)(~10rows)
好多了。
FMDB的某处加了缓存statements的能力,所以当创建或打开一个数据库的时候,我总是调用[self.database setShouldCacheStatements:YES] 。这意味着对每个调用你不需要再次编译每个statement。
我从来没有找到使用vacuum的好的指引,如果数据库没有定期压缩,它会越来越慢。我的应用会跑一个vacuum,但只是每周一次(它在NSUserDefaults里存储上次vacuum的时间,然后在开始的时候检查是否过了一周)。
其他酷的东西
- selectdisplayName,keyfromitemswhereUTTypeConformsTo(uti,?)orderby2;
最后
你真的应该使用Core Data,我不是在开玩笑。
我用sqlite和FMDB一段时间了,我对多得的好处感到很兴奋,也得到非同一般的性能。但是记住机器在变快,其他看你代码的人期望看到他已经知道的Core Data,另一些不打算看你的数据库代码。所以请把这整篇文章看做一个疯子的叫喊,关于他为自己建立的细节的疯狂的世界,并把自己锁在里面。
请享受了不起的Core Data的文章(有点难过的摇头)。