【原】FMDB源码阅读(三)

本文转载请注明出处 —— polobymulberry-博客园

1. 前言


FMDB比较优秀的地方就在于对多线程的处理。所以这一篇主要是研究FMDB的多线程处理的实现。而FMDB最新的版本中主要是通过使用FMDatabaseQueue这个类来进行多线程处理的。

2. FMDatabaseQueue使用举例


// 创建,最好放在一个单例的类中
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath]; // 使用
[queue

inDatabase

:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]]; FMResultSet *rs = [db executeQuery:@"select * from foo"];
while ([rs next]) {
// …
}
}]; // 如果要支持事务
[queue

inTransaction

:^(FMDatabase *db, BOOL *rollback) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]]; if (whoopsSomethingWrongHappened) {
*rollback = YES;
return;
}
// etc…
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
}];

我们可以看到FMDB的多线程实现主要是依赖于FMDatabaseQueue这个类。下面我们结合上面这个例子,来具体看看FMDatabaseQueue的内部实现。

2.1 + [FMDatabaseQueue databaseQueueWithPath:]

// 调用initWithPath:函数构建一个FMDatabaseQueue对象
+ (instancetype)databaseQueueWithPath:(NSString*)aPath {
FMDatabaseQueue *q = [[self alloc] initWithPath:aPath];
FMDBAutorelease(q);
return q;
}

查看initWithPath:函数,发现其本质是调用 - (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName函数。

// 使用aPath作为数据库名称,并传入openFlags和vfsName作为openWithFlags:vfs:函数的参数
// 初始化一个database和相应的queue
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName {
// 除了另外定义了一个_queue外,其他部分和FMDatabase的初始化没什么不同
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这个GCD队列指定了一个kDispatchQueueSpecificKey字符串,并和self(即当前FMDatabaseQueue对象)进行绑定。日后可以通过此字符串获取到绑定的对象(此处就是self)。当然,你要保证正在执行的GCD队列是你之前指定的那个_queue队列。是不是有objc_setAssociatedObject函数的感觉。
此步骤的作用后面inDatabase函数中会具体讲解。
*/
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
_openFlags = openFlags;
} return self;
}

2.2 – [FMDatabaseQueue inDatabase:]

注意inDatabase的参数是一个block。这个block一般是封装了数据库的操作,另外这个block在inDatabase中是同步执行的。

- (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);
// 在当前这个queue中同步执行block
dispatch_sync(_queue, ^() { FMDatabase *db = [self database];
block(db);
// 下面这部分你也看到了,定义了DEBUG宏,明显是用来调试用的。就不赘述了
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);
}

其实我们从这个函数中就可以看出FMDatabaseQueue具体是怎么完成多线程的:

2.3 – [FMDatabaseQueue inTransaction:]

该函数主要是针对数据库事务的处理:

- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block {
[self beginTransaction:NO withBlock:block];
}

可以看到,内部直接封装的是beginTransaction:withBlock:函数,那我们直接来看beginTransaction:withBlock:函数。

- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block {
FMDBRetain(self);
dispatch_sync(_queue, ^() { BOOL shouldRollback = NO; if (useDeferred) {
// 如果使用延迟事务,那么就调用该函数,下面有对该函数的详解
           // 想令useDeferred为YES,可以调用与inTransaction相对的inDeferredTransaction函数
[[self database] beginDeferredTransaction];
}
else {
// 默认使用排他事务,下面有排他事务的详解
[[self database] beginTransaction];
}
// 注意该block除了要创建相应的数据库事务,还需要根据需要选择是否需要回滚
// 比如上面如果数据库操作出错了,那么你可以设置需要回滚,即返回shouldRollback为YES
block([self database], &shouldRollback);
// 如果需要回滚,那么就调用FMDatabase的rollback函数
if (shouldRollback) {
[[self database] rollback];
}
// 如果不需要回滚,那么就调用FMDatabase的commit函数确认提交相应SQL操作
else {
[[self database] commit];
}
}); FMDBRelease(self);
} // 通过执行rollback transaction语句来执行回滚操作
- (BOOL)rollback {
BOOL b = [self executeUpdate:@"rollback transaction"];
// 既然已经回滚了,那么表示是否在进行事务的_inTransaction属性也要置为NO
if (b) {
_inTransaction = NO;
} return b;
}
// 通过执行commit transaction语句来执行提交事务操作
- (BOOL)commit {
BOOL b = [self executeUpdate:@"commit transaction"];
// 既然已经提交过事务了,那么表示是否在进行事务的_inTransaction属性也要置为NO
if (b) {
_inTransaction = NO;
} return b;
}
// 延迟事务指的是在对数据库操作前不进行任何加锁。默认情况下,
// 如果仅仅用BEGIN开始一个事务,那么事务就是DEFERRED的,同时它不会获取任何锁
- (BOOL)beginDeferredTransaction { BOOL b = [self executeUpdate:@"begin deferred transaction"];
if (b) {
_inTransaction = YES;
} return b;
} // 默认进行的是排他(exclusive)操作
// 排他操作的实质是在开始对数据库读写前,获得EXCLUSIVE锁,即排他锁。排它锁说白点就是
// 告诉数据库别的连接:你们不要追她了,她是我老婆了。
- (BOOL)beginTransaction { BOOL b = [self executeUpdate:@"begin exclusive transaction"];
if (b) {
_inTransaction = YES;
} return b;
}

2.4 – [FMDatabaseQueue inSavePoint:]

savepoint类似于游戏存档一样的东西,一般的rollback相当于游戏重新开始,而加了savepoint后,相当于回到存档的位置然后接着游戏。与inDatabase和inTransaction相对有一个inSavePoint:的方法(相当于加了save point功能的inDatabase函数)。

/*
save point功能只在SQLite3.7及以上版本中使用,所以下面多数代码加上了
#if SQLITE_VERSION_NUMBER >= 3007000
#else
#endif
*/
- (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block {
#if SQLITE_VERSION_NUMBER >= 3007000
static unsigned long savePointIdx = ;
__block NSError *err = 0x00;
FMDBRetain(self);
// 同步执行
dispatch_sync(_queue, ^() {
// 设定savepoint的名称,即给游戏存档设一个名字
NSString *name = [NSString stringWithFormat:@"savePoint%ld", savePointIdx++];
// 默认不回滚
BOOL shouldRollback = NO;
// 在执行block之前,先进行存档(save point)。如果有问题,直接退回这个存档(save point)
if ([[self database] startSavePointWithName:name error:&err]) { block([self database], &shouldRollback);
// 如果需要回滚,调用rollbackToSavePointWithName:error:回滚到存档位置(savepoint)
if (shouldRollback) {
[[self database] rollbackToSavePointWithName:name error:&err];
}
// 记得执行完block后,不管有没有回滚,还需要释放掉这个存档
[[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: userInfo:@{NSLocalizedDescriptionKey : errorMessage}];
#endif
}
// 调用savepoint $savepointname的SQL语句对数据库操作进行存档
- (BOOL)startSavePointWithName:(NSString*)name error:(NSError**)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name); NSString *sql = [NSString stringWithFormat:@"savepoint '%@';", FMDBEscapeSavePointName(name)]; return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}
// 使用release savepoint $savepointname的SQL语句删除存档,主要是为了释放资源
- (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError**)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name); NSString *sql = [NSString stringWithFormat:@"release savepoint '%@';", FMDBEscapeSavePointName(name)]; return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}
// 调用rollback transaction to savepoint $savepointname的SQL语句来回退到存档处
- (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError**)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name); NSString *sql = [NSString stringWithFormat:@"rollback transaction to savepoint '%@';", FMDBEscapeSavePointName(name)]; return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}

3. FMDatabasePool(建议使用FMDatabaseQueue)


Tip:

除非你真的知道在什么情况下(比如所有操作均为读操作)可以使用FMDatabasePool,否则尽量改用FMDatabaseQueue,不然可能会引起死锁。

4. 总结


FMDB比较常用的几个类基本上学习完毕。FMDB代码上不是很难,核心还是SQLite3和数据库的知识。更重要的还是要知道真实环境中的最佳实践。

5. 参考文献


【原】FMDB源码阅读(三)的更多相关文章

  1. 【原】FMDB源码阅读(二)

    [原]FMDB源码阅读(二) 本文转载请注明出处 -- polobymulberry-博客园 1. 前言 上一篇只是简单地过了一下FMDB一个简单例子的基本流程,并没有涉及到FMDB的所有方方面面,比 ...

  2. 【原】FMDB源码阅读(一)

    [原]FMDB源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 说实话,之前的SDWebImage和AFNetworking这两个组件我还是使用过的,但是对于 ...

  3. 25 BasicUsageEnvironment0基本使用环境基类——Live555源码阅读(三)UsageEnvironment

    25 BasicUsageEnvironment0基本使用环境基类——Live555源码阅读(三)UsageEnvironment 25 BasicUsageEnvironment0基本使用环境基类— ...

  4. 26 BasicUsageEnvironment基本使用环境——Live555源码阅读(三)UsageEnvironment

    26 BasicUsageEnvironment基本使用环境--Live555源码阅读(三)UsageEnvironment 26 BasicUsageEnvironment基本使用环境--Live5 ...

  5. 24 UsageEnvironment使用环境抽象基类——Live555源码阅读(三)UsageEnvironment

    24 UsageEnvironment使用环境抽象基类——Live555源码阅读(三)UsageEnvironment 24 UsageEnvironment使用环境抽象基类——Live555源码阅读 ...

  6. (原)NSQ源码阅读和分析(1)

    原文出处:https://www.cnblogs.com/lihaiping/p/12324371.html 本文记录自己在阅读和学习nsq源码的时候的一些学习笔记,主要目的是个人总结和方便后期查阅. ...

  7. SparkSQL(源码阅读三)

    额,没忍住,想完全了解sparksql,毕竟一直在用嘛,想一次性搞清楚它,所以今天再多看点好了~ 曾几何时,有一个叫做shark的东西,它改了hive的源码...突然有一天,spark Sql突然出现 ...

  8. FMDB源码阅读

    http://www.cnblogs.com/polobymulberry/p/5178770.html

  9. SpringMVC源码阅读(三)

    先理一下Bean的初始化路线 org.springframework.beans.factory.support.AbstractBeanDefinitionReader public int loa ...

随机推荐

  1. 读书笔记:《HTML5开发手册》--HTML5新的结构元素

    读书笔记:<HTML5开发手册> (HTML5 Developer's CookBook) 虽然从事前端开发已有很长一段时间,对HTML5标签也有使用,但在语义化上面理解还不够清晰.之前在 ...

  2. 【小程序分享篇 二 】web在线踢人小程序,维持用户只能在一个台电脑持登录状态

    最近离职了, 突然记起来还一个小功能没做, 想想也挺简单,留下代码和思路给同事做个参考. 换工作心里挺忐忑, 对未来也充满了憧憬与担忧.(虽然已是老人, 换了N次工作了,但每次心里都和忐忑). 写写代 ...

  3. VisualVM通过jstatd方式远程监控远程主机

    配置好权限文件 [root@test bin]# cd $JAVA_HOME/bin [root@test bin]# vim jstatd.all.policy grant codebase &qu ...

  4. “.Net 社区虚拟大会”(dotnetConf) 2016 Day 3 Keynote: Scott Hanselman

    美国时间 6月7日--9日,为期三天的微软.NET社区虚拟大会正式在 Channel9 上召开,美国时间6.9 是第三天, Scott Hanselman 做Keynote.今天主题围绕的是.NET ...

  5. LeetCode: 3Sum

    Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0? Find all un ...

  6. 立即执行函数表达式(IIFE)

    原文地址:benalman.com/news/2010/11/immediately-invoked-function-expression/ 译者:nzbin 也许你还没有注意到,我是一个对术语比较 ...

  7. WebApi接口 - 响应输出xml和json

    格式化数据这东西,主要看需要的运用场景,今天和大家分享的是webapi格式化数据,这里面的例子主要是输出json和xml的格式数据,测试用例很接近实际常用情况:希望大家喜欢,也希望各位多多扫码支持和点 ...

  8. Hawk 5.1 数据导入和导出

    除了一般的数据库导入导出,Hawk还支持从文件导入和导出,支持的文件类型包括: Excel CSV(逗号分割文本文件) TXT (制表符分割文本文件) Json xml Excel 目前来看,Exce ...

  9. 【夯实PHP基础】nginx php-fpm 输出php错误日志

    本文地址 原文地址 分享提纲: 1.概述 2.解决办法(解决nginx下php-fpm不记录php错误日志) 1. 概述 nginx是一个web服务器,因此nginx的access日志只有对访问页面的 ...

  10. jQuery遮罩层登录对话框

    用户登录是许多网站必备的功能.有一种方式就是不管在网站的哪个页面,点击登录按钮就会弹出一个遮罩层,显示用户登录的对话框.这用方式比较灵活方便.而现在扫描二维码登录的方式也是很常见,例如QQ.微信.百度 ...