IOS开发源码阅读篇--FMDB源码分析3(FMDatabaseQueue+FMDatabasePool)
来源:互联网 发布:sql select 多表查询 编辑:程序博客网 时间:2024/06/05 11:48
一、前言
如上一章所讲,FMDB源码主要有以下几个文件组成:
FMResultSet : 表示FMDatabase执行查询之后的结果集。
FMDatabase : 表示一个单独的SQLite数据库操作实例,通过它可以对数据库进行增删改查等等操作。
FMDatabaseAdditions : 扩展FMDatabase类,新增对查询结果只返回单个值的方法进行简化,对表、列是否存在,版本号,校验SQL等等功能。
FMDatabaseQueue : 使用串行队列 ,对多线程的操作进行了支持。
FMDatabasePool : 使用任务池的形式,对多线程的操作提供支持。(不过官方对这种方式并不推荐使用,优先选择FMDatabaseQueue的方式:ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD)
FMDB比较优秀的地方就在于对多线程的处理。所以这一篇主要是研究FMDB的多线程处理的实现。而FMDB最新的版本中主要是通过使用FMDatabaseQueue这个类来进行多线程处理的。
二、FMDatabaseQueue源码分析
我们先来看看FMDatabaseQueue如何使用。
/** * FMDatabaseQueue使用案例 */- (void)FMDatabaseQueueTest{ //1、获取数据库文件路径 NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; NSString *fileName = [doc stringByAppendingPathComponent:@"students.sqlite"]; //使用 FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:fileName]; [queue inDatabase:^(FMDatabase *db) { [db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_student_2 (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, age integer NOT NULL);"]; [db executeUpdate:@"INSERT INTO t_student_2 (name, age) VALUES ('yixiangZZ', 20);"]; [db executeUpdate:@"INSERT INTO t_student_2 (name, age) VALUES ('yixiangXX', 25);"]; FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_student_2"]; NSLog(@"%@",[NSThread currentThread]); while ([rs next]) { int ID = [rs intForColumn:@"id"]; NSString *name = [rs stringForColumn:@"name"]; int age = [rs intForColumn:@"age"]; NSLog(@"%d %@ %d",ID,name,age); } }]; //支持事务 [queue inTransaction:^(FMDatabase *db, BOOL *rollback) { [db executeUpdate:@"UPDATE t_student_2 SET age = 40 WHERE name = 'yixiangZZ'"]; [db executeUpdate:@"UPDATE t_student_2 SET age = 45 WHERE name = 'yixiangXX'"]; BOOL hasProblem = NO; if (hasProblem) { *rollback = YES;//回滚 return; } FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_student_2"]; NSLog(@"%@",[NSThread currentThread]); while ([rs next]) { int ID = [rs intForColumn:@"id"]; NSString *name = [rs stringForColumn:@"name"]; int age = [rs intForColumn:@"age"]; NSLog(@"%d %@ %d",ID,name,age); } }];}
FMDB的多线程支持实现主要是依赖于FMDatabaseQueue这个类。下面我们来看看他是如何实现的。
2.1:初始化Queue。生成一个串行队列。
+ (instancetype)databaseQueueWithPath:(NSString*)aPath { FMDatabaseQueue *q = [[self alloc] initWithPath:aPath]; FMDBAutorelease(q); return q;}- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName { self = [super init]; if (self != nil) { _db = [[[self class] databaseClass] databaseWithPath:aPath]; FMDBRetain(_db);#if SQLITE_VERSION_NUMBER >= 3005000 BOOL success = [_db openWithFlags:openFlags vfs:vfsName];#else BOOL success = [_db open];#endif if (!success) { NSLog(@"Could not create database queue for path %@", aPath); FMDBRelease(self); return 0x00; } _path = FMDBReturnRetained(aPath); //生成一个串行队列。 _queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL); //给当前queue生成一个标示,给_queue这个GCD队列指定了一个kDispatchQueueSpecificKey字符串,并和self(即当前FMDatabaseQueue对象)进行绑定。日后可以通过此字符串获取到绑定的对象(此处就是self)。 dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL); _openFlags = openFlags; } return self;}
2.2:串行执行数据库操作。
- (void)inDatabase:(void (^)(FMDatabase *db))block { /* 使用dispatch_get_specific来查看当前queue是否是之前设定的那个_queue,如果是的话,那么使用kDispatchQueueSpecificKey作为参数传给dispatch_get_specific的话,返回的值不为空,而且返回值应该就是上面initWithPath:函数中绑定的那个FMDatabaseQueue对象。有人说除了当前queue还有可能有其他什么queue?这就是FMDatabaseQueue的用途,你可以创建多个FMDatabaseQueue对象来并发执行不同的SQL语句。 另外为啥要判断是不是当前执行的这个queue?是为了防止死锁! */ FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey); assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock"); FMDBRetain(self); dispatch_sync(_queue, ^() {//串行执行block FMDatabase *db = [self database]; block(db); if ([db hasOpenResultSets]) {//调试代码 NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]");#if defined(DEBUG) && DEBUG NSSet *openSetCopy = FMDBReturnAutoreleased([[db valueForKey:@"_openResultSets"] copy]); for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) { FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue]; NSLog(@"query: '%@'", [rs query]); }#endif } }); FMDBRelease(self);}
之于为什么要用dispatch_queue_set_specific和dispatch_get_specific判断是不是当前queue,是因为为了防止多线程操作时候出现死锁。可以参考告诉你dispatch_queue_set_specific和dispatch_get_specific是个什么鬼和被废弃的dispatch_get_current_queue。
我们可以看出,一个queue就是一个串行队列。就算你开启多线程执行,它依然还是串行执行的。保证的线程的安全性。看下面一个案例:
/** * 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要高效得多。 */}
执行结果如下,可以看出队列内部就算是异步执行,但是依然还是串行执行的:
)
虽然每个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]); } }]; }); //新建多个队列操作同一个 就不发保证线程安全了。不过一般 不会这么用。}
执行结果如下,可以看出每个队列内部是串行执行的,队列之间的并行执行的:
所以我们可以得到如下结论。
抛出一个问题:如果后台在执行大量的更新,而主线程也需要访问数据库,虽然要访问的数据量很少,但是在后台执行完之前,还是会阻塞主线程。 怎么办?(转载于:FMDB 在多线程中的使用)
解决方法:对此,robertmryan给出了一些想法:
- 如果你是在后台使用的
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... }];});
这种方式能解决不依赖于数据库返回的结果的情况,如果对返回结果有依赖,就需要考虑UI上的体验了,如加一个UIActivityIndicatorView
。
2.3:事务的实现
数据库中的事务 也是保证数据库安全的一种手段。一段sql语句,要么全部成功,要么全部不成功。
- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block { [self beginTransaction:NO withBlock:block];}- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block { FMDBRetain(self); dispatch_sync(_queue, ^() { //串行执行,保证线程安全。 BOOL shouldRollback = NO; if (useDeferred) { [[self database] beginDeferredTransaction];// 使用延时性事务 } else { [[self database] beginTransaction];// 默认使用独占性事务 } block([self database], &shouldRollback);//执行block if (shouldRollback) { //根据shouldRollback判断 是否回滚,还是提交。 [[self database] rollback]; } else { [[self database] commit]; } }); FMDBRelease(self);}
关于延时性事务和独占性事务的区别如下:
在SQLite 3.0.8或更高版本中,事务可以是延迟的,即时的或者独占的。“延迟的”即是说在数据库第一次被访问之前不获得锁。 这样就会延迟事务,BEGIN语句本身不做任何事情。直到初次读取或访问数据库时才获取锁。对数据库的初次读取创建一个SHARED锁 ,初次写入创建一个RESERVED锁。由于锁的获取被延迟到第一次需要时,别的线程或进程可以在当前线程执行BEGIN语句之后创建另外的事务 写入数据库。若事务是即时的,则执行BEGIN命令后立即获取RESERVED锁,而不等数据库被使用。在执行BEGIN IMMEDIATE之后, 你可以确保其它的线程或进程不能写入数据库或执行BEGIN IMMEDIATE或BEGIN EXCLUSIVE. 但其它进程可以读取数据库。 独占事务在所有的数据库获取EXCLUSIVE锁,在执行BEGIN EXCLUSIVE之后,你可以确保在当前事务结束前没有任何其它线程或进程 能够读写数据库。
2.4:存档与回滚
- (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block {#if SQLITE_VERSION_NUMBER >= 3007000 static unsigned long savePointIdx = 0; __block NSError *err = 0x00; FMDBRetain(self); dispatch_sync(_queue, ^() { NSString *name = [NSString stringWithFormat:@"savePoint%ld", savePointIdx++]; BOOL shouldRollback = NO; if ([[self database] startSavePointWithName:name error:&err]) {//设置一个存档点 block([self database], &shouldRollback); if (shouldRollback) { // We need to rollback and release this savepoint to remove it [[self database] rollbackToSavePointWithName:name error:&err];//回滚到存档点 } [[self database] releaseSavePointWithName:name error:&err];//释放该存档 } }); FMDBRelease(self); return err;#else NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil); if (self.logsErrors) NSLog(@"%@", errorMessage); return [NSError errorWithDomain:@"FMDatabase" code:0 userInfo:@{NSLocalizedDescriptionKey : errorMessage}];#endif}
三、FMDatabasePool
FMDatabasePool : 使用任务池的形式,对多线程的操作提供支持。
不过官方对这种方式并不推荐使用(ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD),优先选择FMDatabaseQueue的方式。
平时基本也不使用,官方也不推荐使用。这里就不多讲了。
四、参考资料
FMDB源码
FMDB源码阅读系列
五、联系方式
微博:新浪微博
博客:http://blog.csdn.net/yixiangboy
github:https://github.com/yixiangboy
- IOS开发源码阅读篇--FMDB源码分析3(FMDatabaseQueue+FMDatabasePool)
- IOS开发源码阅读篇--FMDB源码分析1(FMResultSet)
- IOS开发源码阅读篇--FMDB源码分析2(FMDatabase+FMDatabaseAdditions)
- FMDB源码阅读
- FMDB源码分析
- FMDB-FMDatabaseQueue
- FMDB-FMDatabaseQueue
- 【iOS开发-105】SQLite第三方框架FMDB的使用,以及使用FMDatabaseQueue保证线程安全
- FMDB使用Queue:FMDatabaseQueue
- 源码阅读分析
- HASHMAP源码阅读分析
- 源码阅读分析
- zabbix源码阅读分析
- [iOS]MJExtension 源码阅读
- RDVTabBarController【iOS源码阅读】
- Tomcat源码阅读之StandarWrapper源码分析
- (源码阅读)源码分析之AsyncTask
- 阅读《STL源码分析》有感
- 学习感悟
- linux下的ss+privoxy代理配置
- IOS开发源码阅读篇--FMDB源码分析2(FMDatabase+FMDatabaseAdditions)
- ListView中的每个条目如何跳转到另外的Activity?
- git 错误 fatal: Not a valid object name: 'master'.
- IOS开发源码阅读篇--FMDB源码分析3(FMDatabaseQueue+FMDatabasePool)
- 层次遍历二叉树
- android源码分析之JNI调用与回调
- 【UNET自学日志】Part14 射击那些僵尸
- ubuntu opencv 安装编译问题
- spark rdd 去括号
- 微博timeline初步认知
- 前端正则表达式
- Java导入数据到Excel表格(poi方式)