SQLite
SQLite是使用最多的数据库引擎,并且是开源的。它实现了无配置,无服务要求的事务数据库引擎。SQLite可以在Mac OS-X, iOS, Android, Linux, 和 Windows上使用,可以被存储在跨平台磁盘文件的完善的数据库
SQLite的优点:
- 独立于服务器
- 零配置
- 多进程和线程下安全访问。
- 在表中使用含有特殊数据类型的一列或多列存储数据。
Core Data (推荐第三方:MagicalRecord)
Core Data 是App开发者可以使用的第二大主要的IOS存储技术。你需要根据数据类型和数据量进行管理和存储,SQLite和Core Data都有它们各自的优缺点。Core Data 更加关注于对象而不是传统的表数据库方法。使用Core Data,你可以存储一个Objective-C类的对象,(Core Data本身就是基于sqlite的封装,所以它的底层仍然是使用sqlite进行存储数据的)。
尽管它们从本质上不相同,但是CoreData:
- 比SQLite使用更多的内存、更多的存储空间。
- 比SQLite在取数据方面更快,不需要书写大量的sql语句。
- CoreData 的数据模型升级兼容性较差,如果模型不对,会导致程序连起都起不来。虽然提供了模型升级代码,但是在客户端的管理模型版本管理也会相对复杂
- CoreData的好处是集成化,数据动态加载,icloud,包括上层的数据显示,都不需要重新进行数据装配,可以直接使用。
- CoreData,建立的表没有主键,添加时都要自己处理。它不是关系型数据库,处理多对多的关系时比较麻烦。
- CoreData的一个比较大的痛点是多人合作开发的时候,管理CoreData的模型需要很小心,尤其是合并的时候,他的data model是XML格式的,手动resolve比较烦
- CoreData还有其他sql所不具备的优点,比如对undo的支持,多个context实现sketchbook类似的功能,为ManagedObject优化的row cash等。
- CoreData是支持多线程,但需要thread confinement的方式实现,使用了多线程之后可以最大化的防止阻塞主线程
- CoreData自定义升级麻烦,并且效率非常低下,升级的版本变动很难跟踪,有着各种限制(抛弃它主要原因),有些限制直接导致了没法继续开发下去
iOS 8之前,Core Data没有批处理,批量插入,删除,更新效率要比sqlite低很多(这也正是很多程序员因为Core Data批量更新数据效率之低而不得不放弃使用它的原因)。 iOS8之后的Core Data 更新,正好的解决了这个痛点: Batch Updates. Batch Updates在处理大量数据时速度明显提升.Batch Updates可用于批量快速更新数据,Asynchronous Fetching可用于异步抓取海量数据,并可以通过NSProgress实现进度跟踪和取消。
Batch Updates
在CoreData中想要更新大量数据,我们往往要将大量修改后的NSManagedObject
加载到NSManagedObjectContext
中并保存,这会占用大量内存,试想想在iPhone这样的内存有限的移动设备上将是个灾难,数据有可能丢失。你可能会采取批处理的方式,即一小批一小批的更新NSManagedObject
并保存到NSManagedObjectContext
中,但这样会花费很多时间,用户体验较差。
为了解决这个问题,苹果在NSManagedObjectContext
加入了一个新的方法:executeRequest:error:
,它接受一个NSPersistentStoreRequest
类型的参数,返回类型为NSPersistentStoreResult
。
关于NSPersistentStoreRequest
有些人可能比较熟悉,它是NSFetchRequest、NSSaveChangesRequest、NSBatchUpdateRequest
和NSAsynchronousFetchRequest
的基类。后两个类是这次iOS8新加的
NSPersistentStoreResult是一个新加入的类,它也是一个基类,而且是抽象类,这个类作为
executeRequest:error:返回内容的父类,相当于一个接口,它目前有两个子类:
NSPersistentStoreAsynchronousResult和
NSBatchUpdateResult`。
NSBatchUpdateResult
对应着前面的NSBatchUpdateRequest
。NSBatchUpdateRequest
,它有点像NSFetchRequest:
它允许你指定一个想要更新数据的实体;也可以指定一个affectedStores
,它存储了一个接受更新请求的NSPersistentStore
数组。(其实它是NSPersistentStoreRequest
的属性);它也有一个谓词属性来做更新的条件,它跟NSFetchRequest
中的谓词一样强大和灵活,类似于SQL的where语句;它允许你指定想要更新的字段,通过propertiesToUpdate属性来描述字段更新,它是一个字段,key为NSPropertyDescription或属性名字符串,value为NSExpression或常量。
NSBatchUpdateResult
,它有一个result
属性和resultType
属性,result
中的内容跟resultType
有关,可能是成功或者失败,有可能是每行被更新的ID,也可能是被更新的行数。
需要注意的是,由于NSBatchUpdateRequest
并不会先将数据存入内存,而是直接操作数据库,所以并不会引起NSManagedObjectContext
的同步更新,所以你不仅需要获取NSBatchUpdateResult
然后刷新NSManagedObjectContext
对应的数据和UI界面,还需要保证更新后的数据满足数据库模型上的validation
,因为NSManagedObjectContext
没有感知Batch Updates
,一些数据验证工作就落在了程序员的身上(你需要写一段代码验证更新后的数据是合法的,用户可不希望在跑步APP上看到自己今天跑步里程是个负数)。一旦有非法数据录入数据库,下次加载并修改NSManagedObject
的时候就会导致数据验证失败。除了上面提到的这些,还要注意Batch Updates对数据库的操作是乐观锁,也就是假定很少会发生同时存取同一块数据的情况,所以你需要制定一个合理的”merge”策略来应付因同时更新数据产生的冲突。
Batch Updates的优势在于其效率,在处理上万条数据的时候,它执行的时间跟SQL语句执行时间相当。
Asynchronous Fetching
Asynchronous Fetching 的加入依然是为了解决CoreData读取海量数据所带来的问题。通过使用 Asynchronous Fetching,我们可以在抓取数据的同时不阻塞占用 NSManagedObjectContext
,并可以随时取消抓取行为,随时跟踪抓取数据的进度。
设想我们平时用NSFetchRequest
抓取数据的时候,我们会先用NSManagedObjectContext
的executeFetchRequest:error:
方法传入一个NSFetchRequest
,然后请求会被发送到NSPersistentStore
,然后执行一段时间后返回一个数组,在NSManagedObjectContext
更新后,这个数组被当做executeFetchRequest:error:
的返回值返回到我们这里。
而 Asynchronous Fetching则不同,当我们将一个NSAsynchronousFetchRequest
对象传入executeRequest:error:
方法后会立即返回一个“未来的”NSAsynchronousFetchResult
。NSAsynchronousFetchRequest
初始化时需要传入两个参数赋值给属性:
completionBlock
属性,允许我们在抓取完成后执行回调block;
fetchRequest
属性,类型是NSFetchRequest
。也即是说虽然是异步抓取,其实我们用的还是以前的NSFetchRequest
,当NSFetchRequest
抓取结束后会更新NSManagedObjectContext
,这也就意味着NSManagedObjectContext
的并发类型只能是NSPrivateQueueConcurrencyType
或NSMainQueueConcurrencyType
。
于是当我们用NSAsynchronousFetchRequest
抓取数据时,我们会先用NSManagedObjectContext
的executeRequest:error:
方法传入一个NSAsynchronousFetchRequest
,这个方法在NSManagedObjectContext
上执行时,NSManagedObjectContext
会立即制造并返回一个NSAsynchronousFetchResult
,同时NSAsynchronousFetchRequest
会被发送到NSPersistentStore
。你现在可以继续编辑这个NSManagedObjectContext
中的NSManagedObject
,等到NSPersistentStore
执行请求完毕时会将结果返回给NSAsynchronousFetchResult
的finalResult
属性,更新NSManagedObjectContext
,执行NSAsynchronousFetchRequest
的回调block。
Realm
Realm 是个新技术。Realm比前面提到的数据库解决方案更快,更高效。它是一个跨平台的移动数据库,不仅支持Android,iOS还支持macOS,Linux,ReactNative和Xamarin。
最棒的是你通过两行代码就可以处理所有的工作。Realm相比于SQLite和Core Data而言,更易于安装并且运行的更快。
如果你正在设计一款面向很多用户,有很多记录的程序,那么你从设计的一开始就需要特别注意它的可扩展性。Realm在这方面非常出色,并且能够让你快速的操作大量数据。
想要开始使用Realm,你所需要的仅仅是最低iOS 8或者OS X 10.9的系统。早期版本的系统并不支持本地存储与数据库的全新的简单解决方案。
Realm最主要的优势是:
- 绝对免费
- 快速,简单的使用
- 没有使用限制(待定)
- 为了速度和性能,运行在自己的持久化引擎上
- 代码量少,几乎仅仅相当于sqlite的一半代码量
缺点:
- 类名长度最大57个UTF8字符。
- 属性名长度最大63个UTF8字符。
- NSData及NSString属性不能保存超过16M数据,如果有大的可以分块。
- 对字符串进行排序以及不区分大小写查询只支持"基础拉丁字符集"、"拉丁字符补充集"、"拉丁文扩展字符集 A" 以及"拉丁文扩展字符集 B"(UTF-8 的范围在 0~591 之间)。
- 多线程访问时需要新建新的Realm对象。
- Realm没有自增属性。也就是说对于我们习惯的自增主键,如果确实需要,我们要自己去赋值,如果只要求独一无二, 那么可以设为[[NSUUID UUID] UUIDString],如果还要求用来判断插入的顺序,那么可以用Date。
- Realm支持以下的属性类型:BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData 以及 被特殊类型标记的NSNumber,注意,不支持集合类型,只有一个集合RLMArray,如果服务器传来的有数组,那么需要我们自己取数据进行转换存储。
读取性能测试
图片来源