【原】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. RxJS + Redux + React = Amazing!(译一)

    今天,我将Youtube上的<RxJS + Redux + React = Amazing!>翻译(+机译)了下来,以供国内的同学学习,英文听力好的同学可以直接看原版视频: https:/ ...

  3. 谈谈一些有趣的CSS题目(三)-- 层叠顺序与堆栈上下文知多少

    开本系列,讨论一些有趣的 CSS 题目,抛开实用性而言,一些题目为了拓宽一下解决问题的思路,此外,涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题中有你感觉 ...

  4. 如何一步一步用DDD设计一个电商网站(二)—— 项目架构

    阅读目录 前言 六边形架构 终于开始建项目了 DDD中的3个臭皮匠 CQRS(Command Query Responsibility Segregation) 结语 一.前言 上一篇我们讲了DDD的 ...

  5. 在一个空ASP.NET Web项目上创建一个ASP.NET Web API 2.0应用

    由于ASP.NET Web API具有与ASP.NET MVC类似的编程方式,再加上目前市面上专门介绍ASP.NET Web API 的书籍少之又少(我们看到的相关内容往往是某本介绍ASP.NET M ...

  6. iOS---iOS10适配iOS当前所有系统的远程推送

    一.iOS推送通知简介 众所周知苹果的推送通知从iOS3开始出现, 每一年都会更新一些新的用法. 譬如iOS7出现的Silent remote notifications(远程静默推送), iOS8出 ...

  7. 【翻译】MongoDB指南/CRUD操作(二)

    [原文地址]https://docs.mongodb.com/manual/ MongoDB CRUD操作(二) 主要内容: 更新文档,删除文档,批量写操作,SQL与MongoDB映射图,读隔离(读关 ...

  8. Minor【 PHP框架】1.简介

    1.1 Minor是什么 Minor是一个简单但是优秀的符合PSR4的PHP框架,It just did what a framework should do. 只做一个框架应该做的,简单而又强大! ...

  9. 解读发布:.NET Core RC2 and .NET Core SDK Preview 1

    先看一下 .NET Core(包含 ASP.NET Core)的路线图: Beta6: 2015年7月27日 Beta7: 2015年9月2日 Beta8: 2015年10月15日 RC1: 2015 ...

  10. Android带加减的edittext

    看了网上这样自带加减的edittext写得好复杂,还有各种监听事件,我觉得没有必有.于是我自己写了一个. 我这个edittext仅仅限制整数,每次加减1. public class TestEditT ...