概念:
FMDB 是用于数据存储的框架,它是 iOS 平台下对 SQLite 数据库的封装。FMDB 是面向对象的,它以 OC 的方式封装了 SQLite 的 C 语言 API,使用起来更加方便。
Core Data是 ORM(对象关系映射) 的一种体现,使用Core Data需要用到模型数据的转化,虽然操作简单,不需要直接操作数据库,但是性能没有直接使用SQLite高。但是SQLite使用的时候需要使用c语言中的函数,操作比较麻烦,因此需要对它进行封装。但是如果只是简单地封装,很可能会忽略很多重要的细节,比如如何处理并发以及安全性更问题。
使用第三方框架FMDB,它是对libsqlite3框架的封装,用起来的步骤与SQLite使用类似,并且它对于多线程的同时操作一个表格时进行了处理,也就意味着它是线程安全的。FMDB是轻量级的框架,使用灵活,它是很多企业开发的首选。
重要的类:
-
FMResultSet : 表示FMDatabase执行查询之后的结果集。
-
FMDatabase : 表示一个单独的SQLite数据库操作实例,用来执行SQL语句, 通过它可以对数据库进行增删改查等等操作。
-
FMDatabaseAdditions : 扩展FMDatabase类,新增对查询结果只返回单个值的方法进行简化,对表、列是否存在,版本号,校验SQL等等功能。
-
FMDatabaseQueue : 使用串行队列 ,对多线程的操作进行了支持,用于在多线程中执行多个查询或更新,它是线程安全的。
-
FMDatabasePool : 使用任务池的形式,对多线程的操作提供支持。(不过官方对这种方式并不推荐使用,优先选择FMDatabaseQueue的方式:ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD)
FMDatabaseQueue 要使用单例创建,这样多线程调用时,数据库操作使用一个队列,保证线程安全。
是把数据库的操作放到一个串行队列中,从而保证不会在同一时间对数据库做改动。
多线程下使用FMDatabaseQueue的操作原理就可以创建一个管理类对模型数据的存取查删进行统一管理,可以使用工具类操作,也可以创建集成NSObject的子类进行管理,需要存取的模型类继承此子类即可。
FMDatabaseQueue如何实现多线程?
/** * FMDatabaseQueue如何实现多线程的案例 */ - (void)FMDatabaseQueueMutilThreadTest { //1、获取数据库文件路径 NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; NSString *fileName = [doc stringByAppendingPathComponent:@"students.sqlite"]; //使用queue1 FMDatabaseQueue *queue1 = [FMDatabaseQueue databaseQueueWithPath:fileName]; [queue1 inDatabase:^(FMDatabase *db) { for (int i=0; i<10; i++) { NSLog(@"queue1---%zi--%@",i,[NSThread currentThread]); } }]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [queue1 inDatabase:^(FMDatabase *db) { for (int i=11; i<20; i++) { NSLog(@"queue1---%zi--%@",i,[NSThread currentThread]); } }]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [queue1 inDatabase:^(FMDatabase *db) { for (int i=20; i<30; i++) { NSLog(@"queue1---%zi--%@",i,[NSThread currentThread]); } }]; }); //虽然开启了多个线程,可依然还是串行处理。原因如下: /**FMDatabaseQueue虽然看似一个队列,实际上它本身并不是,它通过内部创建一个Serial的dispatch_queue_t来处理通过inDatabase和inTransaction传入的Blocks,所以当我们在主线程(或者后台)调用inDatabase或者inTransaction时,代码实际上是同步的。FMDatabaseQueue这么设计的目的是让我们避免发生并发访问数据库的问题,因为对数据库的访问可能是随机的(在任何时候)、不同线程间(不同的网络回调等)的请求。内置一个Serial队列后,FMDatabaseQueue就变成线程安全了,所有的数据库访问都是同步执行,而且这比使用@synchronized或NSLock要高效得多。 */ }
//虽然开启了多个线程,可依然还是串行处理。原因如下:
FMDatabaseQueue虽然看似一个队列,实际上它本身并不是,
它通过内部创建一个 Serial 的 dispatch_queue_t 来处理通过 inDatabase 和 inTransaction 传入的 Blocks.
所以当我们在主线程(或者后台)调用 inDatabase 或者 inTransaction 时,代码实际上是同步的。
FMDatabaseQueue这么设计的目的是让我们避免发生并发访问数据库的问题,因为对数据库的访问可能是随机的(在任何时候)、不同线程间(不同的网络回调等)的请求。
内置一个 Serial 队列后,FMDatabaseQueue 就变成线程安全了,所有的数据库访问都是同步执行,而且这比使用 @synchronized 或 NSLock要高效得多。
虽然每个queue内部是串行执行的,当时不同的queue之间可以并发执行。
/** * FMDatabaseQueue如何实现多线程的案例2 */ - (void)FMDatabaseQueueMutilThreadTest2{ //1、获取数据库文件路径 NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; NSString *fileName = [doc stringByAppendingPathComponent:@"students.sqlite"]; //使用queue1 FMDatabaseQueue *queue1 = [FMDatabaseQueue databaseQueueWithPath:fileName]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [queue1 inDatabase:^(FMDatabase *db) { for (int i=0; i<5; i++) { NSLog(@"queue1---%zi--%@",i,[NSThread currentThread]); } }]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [queue1 inDatabase:^(FMDatabase *db) { for (int i=5; i<10; i++) { NSLog(@"queue1---%zi--%@",i,[NSThread currentThread]); } }]; }); //使用queue2 FMDatabaseQueue *queue2 = [FMDatabaseQueue databaseQueueWithPath:fileName]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [queue2 inDatabase:^(FMDatabase *db) { for (int i=0; i<5; i++) { NSLog(@"queue2---%zi--%@",i,[NSThread currentThread]); } }]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [queue2 inDatabase:^(FMDatabase *db) { for (int i=5; i<10; i++) { NSLog(@"queue2---%zi--%@",i,[NSThread currentThread]); } }]; }); //新建多个队列操作同一个 就不发保证线程安全了。不过一般 不会这么用。 }
如果后台在执行大量的更新,而主线程也需要访问数据库,虽然要访问的数据量很少,但是在后台执行完之前,还是会阻塞主线程。 怎么办?
解决方案:
- 如果你是在后台使用的
inDatabase
来执行更新,可以考虑换成inTransaction
,后者比前者更新起来快很多,特别是在更新量比较大的时候(比如更新1000条或10000条)。 - 拆解你的更新数据量,如果有300条,可以分10次、每次更新30条。当然有时不能这么做,因为你可能通过网络请求回来的数据,你希望一次性、完整地写入到数据库中,虽然有局限性,不过这确实能很好地减少每个Block占用数据库的时间。
- 上面两点可以改善问题,但是问题依然是存在的,在大多数时候,你应该把从主线程调用
inDatabase
和inTransaction
放在异步里:dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self.databaseQueue inDatabase:^(FMDatabase *db) { //do something... }]; });