IOS开发数据存储篇--FMDB源码分析3(FMDatabaseQueue+FMDatabasePool)

一、前言

如上一章所讲,FMDB源码主要有以下几个文件组成:

  1. FMResultSet : 表示FMDatabase执行查询之后的结果集。
  2. FMDatabase : 表示一个单独的SQLite数据库操作实例,通过它可以对数据库进行增删改查等等操作。
  3. FMDatabaseAdditions : 扩展FMDatabase类,新增对查询结果只返回单个值的方法进行简化,对表、列是否存在,版本号,校验SQL等等功能。
  4. FMDatabaseQueue : 使用串行队列 ,对多线程的操作进行了支持。
  5. 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给出了一些想法:

  1. 如果你是在后台使用的inDatabase来执行更新,可以考虑换成inTransaction,后者比前者更新起来快很多,特别是在更新量比较大的时候(比如更新1000条或10000条)。
  2. 拆解你的更新数据量,如果有300条,可以分10次、每次更新30条。当然有时不能这么做,因为你可能通过网络请求回来的数据,你希望一次性、完整地写入到数据库中,虽然有局限性,不过这确实能很好地减少每个Block占用数据库的时间。
  3. 上面两点可以改善问题,但是问题依然是存在的,在大多数时候,你应该把从主线程调用inDatabaseinTransaction放在异步里:
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

时间: 2024-10-05 18:37:33

IOS开发数据存储篇--FMDB源码分析3(FMDatabaseQueue+FMDatabasePool)的相关文章

IOS开发数据存储篇—IOS中的几种数据存储方式

IOS开发数据存储篇—IOS中的几种数据存储方式 发表于2016/4/5 21:02:09  421人阅读 分类: 数据存储 在项目开发当中,我们经常会对一些数据进行本地缓存处理.离线缓存的数据一般都保存在APP所在的沙盒之中.一般有以下几种: 1.PList(XML属性列表) 在使用plist进行数据存储和读取,只适用于系统自带的一些常用类型才能用,且必须先获取路径相对麻烦 //写入文件 NSString *doc = [NSSearchPathForDirectoriesInDomains(

【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析

[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何在网关上实现客户端自定义限流功能,基本完成了关于网关的一些自定义扩展需求,后面几篇将介绍基于IdentityServer4(后面简称Ids4)的认证相关知识,在具体介绍ids4实现我们统一认证的相关功能前,我们首先需要分析下Ids4源码,便于我们彻底掌握认证的原理以及后续的扩展需求. .netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论. 一.Ids4文档及源码 文档地址 http:/

IOS开发--数据持久化篇之文件存储(一)

前言:个人觉得开发人员最大的悲哀莫过于懂得使用却不明白其中的原理.在代码之前我觉得还是有必要简单阐述下相关的一些知识点. 因为文章或深或浅总有适合的人群.若有朋友发现了其中不正确的观点还望多多指出,不胜感激. 什么叫数据持久化: 在这里我就不照搬教科书上抽象的概念了.我觉得既然要把东西写出来就让它简单明了. 要搞清楚数据持久化,首先要知道数据持久化是相对于缓存而言的,缓存是在程序运行的过程中保存在内存中,程序一旦运行结束,其内存就会被释放.缓存在内存中的数据也就随之消失. 那么数据持久化就是要解

IOS开发--数据持久化篇文件存储(二)

前言:个人觉得开发人员最大的悲哀莫过于懂得使用却不明白其中的原理.在代码之前我觉得还是有必要简单阐述下相关的一些知识点. 因为文章或深或浅总有适合的人群.若有朋友发现了其中不正确的观点还望多多指出,不胜感激. 承接上篇博客我们来看看IOS开发中是如何将一个自定义的对象进行归档的 本篇博客将介绍以下几个方面的内容 1)普通的单个对象归档操作 2)拥有继承关系的对象归档 3)同时将多个对象进行归档 1.普通的单个对象归档操作 首先我们来看下最简单的单个对象归档操作 1.自定义一个跟小明一样有名的类(

iOS 开发之 FMDB 源码分析

概念: FMDB 是用于数据存储的框架,它是 iOS 平台下对 SQLite 数据库的封装.FMDB 是面向对象的,它以 OC 的方式封装了 SQLite 的 C 语言 API,使用起来更加方便. Core Data是 ORM(对象关系映射) 的一种体现,使用Core Data需要用到模型数据的转化,虽然操作简单,不需要直接操作数据库,但是性能没有直接使用SQLite高.但是SQLite使用的时候需要使用c语言中的函数,操作比较麻烦,因此需要对它进行封装.但是如果只是简单地封装,很可能会忽略很多

iOS开发-数据存储NSCoder

软件中永远绕不开的一个问题就是数据存储的问题,PC的时候一般都是选择在数据库中存储,iOS如果是和后端配合的话,那么不需要考虑数据存储的这个问题,上次写了一下plist的存储,不过数据都是存储一些简单的键值对对象.本次需要将一些自己定义的类型存储在plist比如说图片,这个时候可以利用NSCoding协议,将数据地以类似档案的形式存储到plist文件中,然后从plist的文件中读取数据,使用协议的时候这个时候就会用到了NSCoder,如果对存档和解压没有概念的话,可以简单的理解为数据的序列化与反

IOS开发—数据存储(直接写入、NSUserDefaults、NSkeyedArchiver)

数据存储(直接写入.NSUserDefaults.NSkeyedArchiver) ios中常用文件存取的方法有: 1.直接写文件的方式,可以存储的对象有NSString.NSArray.NSDictionary.NSData.NSNumber,数据全部存放在一个属性列表文件(*.plist文件)中. 2.NSUeserDefaults(偏好设置),用来存储应用设置信息,文件放在perference目录下. 3.归档操作(NSkeyedArchiver),不同于前面两种,它可以把自定义对象存放在

HashSet底层存储元素的源码分析

此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持.它不保证 set 的迭代顺序:特别是它不保证该顺序恒久不变.此类允许使用 null 元素. HashSet<String> set = new HashSet<String>();set.add("abc"); 1 private transient HashMap<E,Object> map; 2 /** 7 * Constructs a new, empty set; t

Java游戏服务器成长之路——弱联网游戏篇(源码分析)

http://blog.csdn.net/hjcenry/article/details/50530472 前段时间由于公司的一款弱联网游戏急着上线,没能及时分享,现在基本做的差不多,剩下的就是测试阶段了(本来说元旦来分享一下服务器技术的).公司的这款游戏已经上线一年多了,在我来之前一直都是单机版本,由于人民群众的力量太强大,各种内购破解,刷体力,刷金币,刷钻石版本的出现,公司才决定将这款游戏转型为弱联网游戏,压制百分之八十的破解用户(毕竟原则上还是属于单机游戏,不可能做到百分之百的防破解),招