FMDB源码分析

一、前言

如上一章所讲,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如何使用。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<code class = "language-objective-c hljs sql" > /**
  *  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);
         }
 
     }];
}</code>

FMDB的多线程支持实现主要是依赖于FMDatabaseQueue这个类。下面我们来看看他是如何实现的。

2.1:初始化Queue。生成一个串行队列。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<code class = "language-objective-c hljs objectivec" >+ (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;
}</code>

2.2:串行执行数据库操作。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<code class = "language-objective-c hljs erlang" >- ( 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);
}</code>

之于为什么要用dispatch_queue_set_specific和dispatch_get_specific判断是不是当前queue,是因为为了防止多线程操作时候出现死锁。可以参考告诉你dispatch_queue_set_specific和dispatch_get_specific是个什么鬼被废弃的dispatch_get_current_queue

我们可以看出,一个queue就是一个串行队列。就算你开启多线程执行,它依然还是串行执行的。保证的线程的安全性。看下面一个案例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<code class = "language-objective-c hljs java" > /**
  *  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要高效得多。
      */
}</code>

执行结果如下,可以看出队列内部就算是异步执行,但是依然还是串行执行的:

 

执行结果1

虽然每个queue内部是串行执行的,当时不同的queue之间可以并发执行

案例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<code class = "language-objective-c hljs objectivec" > /**
  *  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]);
             }
         }];
     });
 
     //新建多个队列操作同一个 就不发保证线程安全了。不过一般 不会这么用。
}</code>

执行结果如下,可以看出每个队列内部是串行执行的,队列之间的并行执行的:

 

\

 

所以我们可以得到如下结论。

 

\

2.3:事务的实现

数据库中的事务 也是保证数据库安全的一种手段。一段sql语句,要么全部成功,要么全部不成功。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<code class = "language-objective-c hljs objectivec" >- ( 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);
}</code>

关于延时性事务和独占性事务的区别如下:

在SQLite 3.0.8或更高版本中,事务可以是延迟的,即时的或者独占的。“延迟的”即是说在数据库第一次被访问之前不获得锁。 这样就会延迟事务,BEGIN语句本身不做任何事情。直到初次读取或访问数据库时才获取锁。对数据库的初次读取创建一个SHARED锁 ,初次写入创建一个RESERVED锁。由于锁的获取被延迟到第一次需要时,别的线程或进程可以在当前线程执行BEGIN语句之后创建另外的事务 写入数据库。若事务是即时的,则执行BEGIN命令后立即获取RESERVED锁,而不等数据库被使用。在执行BEGIN IMMEDIATE之后, 你可以确保其它的线程或进程不能写入数据库或执行BEGIN IMMEDIATE或BEGIN EXCLUSIVE. 但其它进程可以读取数据库。 独占事务在所有的数据库获取EXCLUSIVE锁,在执行BEGIN EXCLUSIVE之后,你可以确保在当前事务结束前没有任何其它线程或进程 能够读写数据库。

2.4:存档与回滚

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<code class = "language-objective-c hljs objectivec" >- (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
}</code>

三、FMDatabasePool

FMDatabasePool : 使用任务池的形式,对多线程的操作提供支持。

不过官方对这种方式并不推荐使用(ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD),优先选择FMDatabaseQueue的方式。

平时基本也不使用,官方也不推荐使用。这里就不多讲了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值