美文网首页
有关FMDB的理解以及源码分析

有关FMDB的理解以及源码分析

作者: hui8685291 | 来源:发表于2020-12-20 23:12 被阅读0次

    FMDB主要有以下几个类:

    (1)FMDatabase:代表一个单独的SQLite操作实例,数据库通过它增删改查操作;

    (2)FMResultSet:代表查询后的结果集;

    (3)FMDatabaseQueue:代表串行队列,对多线程操作提供了支持;

    (4)FMDatabaseAdditions:本类用于扩展FMDatabase,用于查找表是否存在,版本号等功能;

    (5)FMDatabasePool:此方式官方是不推荐使用,代表是任务池,也是对多线程提供了支持。

    .线程安全

    在多个线程中同时使用一个FMDatabase实例是不明智的。不要让多个线程分享同一个FMDatabase实例,它无法在多个线程中同时使用。 如果在多个线程中同时使用一个FMDatabase实例,会造成数据混乱等问题。所以,请使用 FMDatabaseQueue,它是线程安全的。以下是使用方法:

    1.创建

    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;

    NSString *filePath = [path stringByAppendingPathComponent:@"FMDB.db"];

    FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];

    databaseQueueWithPath:方法里面创建了一个串行队列

    2.操作数据

    [queue inDatabase:^(FMDatabase*db) {

    //FMDatabase数据库操作

    if (![db open]) {

    NSLog(@"打开数据库失败");

    return ;

    }

    //创建表(FMDB中只有update和query操作,除了查询其他都是update操作)}];

    [db executeUpdate:@"create table if not exists user(name text,gender text,age integer) "];

    //插入数据

    BOOL inser = [db executeUpdate:@"insert into user values(?,?,?)",_nameTextField.text,_sexTextField.text,_ageTextField.text];

    [db close];

    }

    3.事务操作:

    我们可以这样理解数据库事物:对数据库所做的一系列修改,在修改过程中,暂时不写入数据库,而是缓存起来,用户在自己的终端可以预览变化,直到全部修改完成,并经过检查确认无误后,一次性提交并写入数据库,在提交之前,必要的话所做的修改都可以取消。提交之后,就不能撤销,提交成功后其他用户才可以通过查询浏览数据的变化。
    简单的说也就是,事务可以让多个表的数据同时插入,一旦有一个表操作失败,那么其他表也都会失败。当然这种说法是为了理解,不是严谨的。
    那么对一个表大量插入数据时也可以用事务。比如sqlite3。
    数据库 中 insert into 语句等操作是比较耗时的,假如我们一次性插入几百几千条数据就会造成主线程阻塞,以至于ui界面卡住。那么这时候我们就要开启一个事物来进行操作。
    原因就是它以文件的形式存在磁盘中,每次访问时都要打开一次文件,如果对数据库进行大量的操作,就很慢。可是如果我们用事务的形式提交,开始事务后,进行的大量操作语句都保存在内存中,当提交commit时才全部写入数据库,此时,数据库文件也只用打开一次。如果操作错误,还可以回滚事务。
    // 单线程事务

    //事务
    -(void)transaction
    {
         BOOL isSuccess=[_dataBase open];
        if (!isSuccess) {
            HSLog(@"打开数据库失败");
        }
        [_dataBase beginTransaction];
        BOOL isRollBack = NO;
        @try {
            for (int i = 0; i<500; i++) {
                NSString *nId = [NSString stringWithFormat:@"%d",i];
                NSString *strName = [[NSString alloc] initWithFormat:@"student_%d",i];
                NSString *sql = @"INSERT INTO Student (id,student_name) VALUES (?,?)";
                BOOL a = [_dataBase executeUpdate:sql,nId,strName];
                if (!a) {
                    NSLog(@"插入失败1");
                }
            }
        }
        @catch (NSException *exception) {
            isRollBack = YES;
            [_dataBase rollback];
        }
        @finally {
            if (!isRollBack) {
                [_dataBase commit];
            }
        }
        [_dataBase close];
    }
    

    // 多线程事务

    //事务
    -(void)transaction
    {
        [_dataBase inTransaction:^(FMDatabase *db, BOOL *rollback) {
            for (int i = 0; i<500; i++) {
                NSString *nId = [NSString stringWithFormat:@"%d",i];
                NSString *strName = [[NSString alloc] initWithFormat:@"student_%d",i];
                NSString *sql = @"INSERT INTO Student (id,student_name) VALUES (?,?)";
                BOOL a = [db executeUpdate:sql,nId,strName];
                if (!a) {
                    *rollback = YES;
                    return;
                }
            }
        }];
    
    }
    

    + [FMDatabase databaseWithPath:]

    // 核心其实还是调用了+[FMDataBase initWithPath:]函数,下面会详解
    + (instancetype)databaseWithPath:(NSString*)aPath {
        // FMDBReturnAutoReleased是为了让FMDB兼容MRC和ARC,具体细节看下其宏定义就明白了
        return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath]);
    }
     
    /** 初始化一个FMDataBase对象
     根据path(aPath)来创建一个SQLite数据库。对应的aPath参数有三种情形:
     
     1. 数据库文件路径:不为空字符串,不为nil。如果该文件路径不存在,那么SQLite会给你新建一个;
     2. 空字符串@"":将在外存临时给你创建一个空的数据库,并且如果该数据库连接释放,那么对应数据库会自动删除;
     3. nil:会在内存中创建数据库,随着该数据库连接的释放,也会释放该数据库;
    
    - (instancetype)initWithPath:(NSString*)aPath {
        // SQLite支持三种线程模式,sqlite3_threadsafe()函数的返回值可以确定编译时指定的线程模式。
        // 三种模式分别为1.单线程模式 2.多线程模式 3.串行模式 其中对于单线程模式,sqlite3_threadsafe()返回false
        // 对于另外两个模式,则返回true。这是因为单线程模式下没有进行互斥(mutex),所以多线程下是不安全的
        assert(sqlite3_threadsafe());
        self = [super init];
        // 很多属性后面再提。不过这里值得注意的是_db居然赋值为nil,也就是说真正构建_db不是在initWithPath:这个函数中,这里透露下,其实作者是将构建部分代码放到了open函数中if (self) {
            _databasePath               = [aPath copy];
            _openResultSets             = [[NSMutableSet alloc] init];
            _db                         = nil;
            _logsErrors                 = YES;
            _crashOnErrors              = NO;
            _maxBusyRetryTimeInterval   = ;
        }
     
        return self;
    }
    
    

    + [FMDatabase open]

    上面提到过+ [FMDatabase databaseWithPath:]和- [FMDatabase initWithPath:]本质上只是给了数据库一个名字,并没有真实创建或者获取数据库。这里的open函数才是真正获取到数据库,其本质上也就是调用SQLite的C/C++接口 – sqlite3_open()
    sqlite3_open(const char *filename, sqlite3 **ppDb)
    该例子打开一个指向 SQLite 数据库文件的连接,返回一个用于其他 SQLite 程序的数据库连接对象。
    如果 filename 参数是 NULL 或 ':memory:',那么 sqlite3_open() 将会在 RAM 中创建一个内存数据库,这只会在 session 的有效时间内持续。
    如果文件名 filename 不为 NULL,那么 sqlite3_open() 将使用这个参数值尝试打开数据库文件。如果该名称的文件不存在,sqlite3_open() 将创建一个新的命名为该名称的数据库文件并打开。

    - (BOOL)open {
        if (_db) {
            return YES;
        }
     
        int err = sqlite3_open([self sqlitePath], (sqlite3**)&_db );
        if(err != SQLITE_OK) {
            NSLog(@"error opening!: %d", err);
            return NO;
        }
        // 若_maxBusyRetryTimeInterval大于0,那么就调用setMaxBusyRetryTimeInterval:函数
        // setMaxBusyRetryTimeInterval:函数主要是调用sqlite3_busy_handler来处理其他线程已经在操作数据库的情况,默认_maxBusyRetryTimeInterval为2。
      // 具体该参数有什么用,下面在FMDBDatabaseBusyHandler函数中会详解。
        if (_maxBusyRetryTimeInterval > 0.0) {
            [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
        }
     
        return YES;
    }
    - (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeout {
     
        _maxBusyRetryTimeInterval = timeout;
     
        if (!_db) {
            return;
        }
        // 处理的handler设置为FMDBDatabaseBusyHandler这个函数
        if (timeout > ) {
            sqlite3_busy_handler(_db, &FMDBDatabaseBusyHandler, (__bridge void *)(self));
        }
        else {
            // 不使用任何busy handler处理
            sqlite3_busy_handler(_db, nil, nil);
        }
    }
    

    这里需要提一下sqlite3_busy_handler这个函数:
    int sqlite3_busy_handler(sqlite3, int()(void,int), void);

    第一个参数是告知哪个数据库需要设置busy handler。

    第二个参数是其实就是回调函数(busy handler)了,当你调用该回调函数时,需传递给它的一个void*的参数的拷贝,也即sqlite3_busy_handler的第三个参数;另一个需要传给回调函数的int参数是表示这次锁事件,该回调函数被调用的次数。如果回调函数返回0时,将不再尝试再次访问数据库而返回SQLITE_BUSY或者SQLITE_IOERR_BLOCKED。如果回调函数返回非0, 将会不断尝试操作数据库。
    总结:程序运行过程中,如果有其他进程或者线程在读写数据库,那么sqlite3_busy_handler会不断调用回调函数,直到其他进程或者线程释放锁。获得锁之后,不会再调用回调函数,从而向下执行,进行数据库操作。该函数是在获取不到锁的时候,以执行回调函数的次数来进行延迟,等待其他进程或者线程操作数据库结束,从而获得锁操作数据库。

    // 注意:appledoc(生成文档的软件)中,对于有具体实现的C函数,比如下面这个函数,
    // 是有bug的。所以你在生成文档时,忽略.m文件。
     
    // 该函数就是简单调用sqlite3_sleep来挂起进程
    static int FMDBDatabaseBusyHandler(void *f, int count) {
        FMDatabase *self = (__bridge FMDatabase*)f;
        // 如果count为0,表示的第一次执行回调函数
        // 初始化self->_startBusyRetryTime,供后面计算delta使用
        if (count == ) {
            self->_startBusyRetryTime = [NSDate timeIntervalSinceReferenceDate];
            return ;
        }
        // 使用delta变量控制执行回调函数的次数,每次挂起50~100ms
        // 所以maxBusyRetryTimeInterval的作用就在这体现出来了
        // 当挂起的时长大于maxBusyRetryTimeInterval,就返回0,并停止执行该回调函数了
        NSTimeInterval delta = [NSDate timeIntervalSinceReferenceDate] - (self->_startBusyRetryTime);
     
        if (delta < [self maxBusyRetryTimeInterval]) {
             // 使用sqlite3_sleep每次当前线程挂起50~100ms
            int requestedSleepInMillseconds = (int) arc4random_uniform() + ;
            int actualSleepInMilliseconds = sqlite3_sleep(requestedSleepInMillseconds);
            // 如果实际挂起的时长与想要挂起的时长不一致,可能是因为构建SQLite时没将HAVE_USLEEP置为1
            if (actualSleepInMilliseconds != requestedSleepInMillseconds) {
                NSLog(@"WARNING: Requested sleep of %i milliseconds, but SQLite returned %i. Maybe SQLite wasn't built with HAVE_USLEEP=1?", requestedSleepInMillseconds, actualSleepInMilliseconds);
            }
            return ;
        }
     
        return ;
    }
    

    [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:](重点)

    [FMDatabase executeQuery:]等等类似的函数,最终都是对- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]的简单封装。该函数比较关键,主要是针对查询的sql语句。

    - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
        // 判断当前是否存在数据库以供操作
        if (![self databaseExists]) {
            return 0x00;
        }
        // 如果当前线程已经在使用数据库了,那就输出正在使用的警告
        if (_isExecutingStatement) {
            [self warnInUse];
            return 0x00;
        }
     
        _isExecutingStatement = YES;
     
        int rc                  = 0x00;
        sqlite3_stmt *pStmt     = 0x00; // sqlite的prepared语句类型
        FMStatement *statement  = 0x00; // 对sqlite3_stmt的简单封装,在实际应用中,你不应直接操作FMStatement对象
        FMResultSet *rs         = 0x00; // FMResultSet对象是用来获取最终查询结果的
        // 需要追踪sql执行状态的话,输出执行状态
        if (_traceExecution && sql) {
            NSLog(@"%@ executeQuery: %@", self, sql);
        }
        // 调用sql语句之前,首先要将sql字符串预处理一下,转化为SQLite可用的prepared语句(预处理语句)
        // 使用sqlite3_prepare_v2来生成sql对应的prepare语句(即pStmt)代价很大
        // 所以建议使用缓存机制来减少对sqlite3_prepare_v2的使用
        if (_shouldCacheStatements) {
            // 获取到缓存中的prepared语句
            statement = [self cachedStatementForQuery:sql];
            pStmt = statement ? [statement statement] : 0x00;
            // prepared语句可以被重置(调用sqlite3_reset函数),然后可以重新绑定参数以便重新执行。
            [statement reset];
        }
        // 如果缓存中没有sql对应的prepared语句,那么只能使用sqlite3_prepare_v2函数进行预处理
        if (!pStmt) {
     
            rc = sqlite3_prepare_v2(_db, [sql UTF8String], -, &pStmt, );
            // 如果生成prepared语句出错,那么就根据是否需要打印错误信息(_logsErrors)以及是否遇到错误直接中止程序执行(_crashOnErrors)来执行出错处理。
            // 最后调用sqlite3_finalize函数释放所有的内部资源和sqlite3_stmt数据结构,有效删除prepared语句。
            if (SQLITE_OK != rc) {
                if (_logsErrors) {
                    NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
                    NSLog(@"DB Query: %@", sql);
                    NSLog(@"DB Path: %@", _databasePath);
                }
     
                if (_crashOnErrors) {
                    NSAssert(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
                  // abort()函数表示中止程序执行,直接从调用的地方跳出。
                    abort();
                }
     
                sqlite3_finalize(pStmt);
                _isExecutingStatement = NO;
                return nil;
            }
        }
     
        id obj;
        int idx = ;
        // 获取到pStmt中需要绑定的参数个数
        int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)
     
        if (dictionaryArgs) {
     
            for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
     
                // 在每个dictionaryKey之前加上冒号,比如上面的a -> :a,方便获取参数在prepared语句中的索引
                NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
                // 查看执行状况
                if (_traceExecution) {
                    NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
                }
     
                // 在prepared语句中查找对应parameterName的参数索引值namedIdx
                int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
     
                FMDBRelease(parameterName);
                 // 可以利用索引namedIdx获取对应参数,再使用bindObject:函数将dictionaryArgs保存的value绑定给对应参数
                if (namedIdx > ) {
                    [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
                    // 使用这个idx来判断sql中的所有参数值是否都绑定上了
                    idx++;
                }
                else {
                    NSLog(@"Could not find index for %@", dictionaryKey);
                }
            }
        }
        else {
     
            while (idx < queryCount) {
                // 使用arrayArgs的例子
                /**
                 [db executeQuery:@"insert into testOneHundredTwelvePointTwo values (?, ?)" withArgumentsInArray:[NSArray arrayWithObjects:@"one", [NSNumber numberWithInteger:2], nil]];
                 */
                if (arrayArgs && idx < (int)[arrayArgs count]) {
                    obj = [arrayArgs objectAtIndex:(NSUInteger)idx];
                }
            // 使用args的例子,使用args其实就是调用- (FMResultSet *)executeQuery:(NSString*)sql, ...;
            /**
              FMResultSet *rs = [db executeQuery:@"select rowid,* from test where a = ?", @"hi'"];
             */
                else if (args) {
                    obj = va_arg(args, id);
                }
                else {
                    break;
                }
     
                if (_traceExecution) {
                    if ([obj isKindOfClass:[NSData class]]) {
                        NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]);
                    }
                    else {
                        NSLog(@"obj: %@", obj);
                    }
                }
     
                idx++;
                // 绑定参数值
                [self bindObject:obj toColumn:idx inStatement:pStmt];
            }
        }
        // 如果绑定的参数数目不对,认为出错,并释放资源
        if (idx != queryCount) {
            NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)");
            sqlite3_finalize(pStmt);
            _isExecutingStatement = NO;
            return nil;
        }
     
        FMDBRetain(statement); // to balance the release below
        // statement不为空,进行缓存
        if (!statement) {
            statement = [[FMStatement alloc] init];
            [statement setStatement:pStmt];
            // 使用sql作为key来缓存statement(即sql对应的prepare语句)
            if (_shouldCacheStatements && sql) {
                [self setCachedStatement:statement forQuery:sql];
            }
        }
     
        // 根据statement和self(FMDatabase对象)构建一个FMResultSet对象,此函数中仅仅是构建该对象,还没使用next等函数获取查询结果
        // 注意FMResultSet中含有以下成员(除了最后一个,其他成员均在此处初始化过了)
        /**
          @interface FMResultSet : NSObject {
               FMDatabase          *_parentDB; // 表示该对象查询的数据库,主要是为了能在FMResultSet自己的函数中索引到正在操作的FMDatabase对象
               FMStatement         *_statement; // prepared语句
     
               NSString            *_query; // 对应的sql查询语句
               NSMutableDictionary *_columnNameToIndexMap;
           }
         */
        rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
        [rs setQuery:sql];
        // 将此时的FMResultSet对象添加_openResultSets,主要是为了调试
        NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
        [_openResultSets addObject:openResultSet];
        // 并设置statement的使用数目useCount加1,暂时不清楚此成员有何作用,感觉也是用于调试
        [statement setUseCount:[statement useCount] + ];
     
        FMDBRelease(statement);
        // 生成statement的操作已经结束
        _isExecutingStatement = NO;
     
        return rs;
    }
    
      举例dictionaryArgs:   NSMutableDictionary 
    *dictionaryArgs = [NSMutableDictionary dictionary]; [dictionaryArgs setObject:@"Text1" forKey:@"a"]; [db executeQuery:@"select * from namedparamcounttest where a = :a" withParameterDictionary:dictionaryArgs]; // 注意类似:AAA前面有冒号的就是参数 // 其他的参数形式如:"?", "?NNN", ":AAA", "$AAA", 或 "@AAA" */
    
    

    [FMResultSet nextWithError:]

    • [FMResultSet next]函数其实就是对nextWithError:的简单封装。作用就是从我们上一步open中获取到的FMResultSet对象中读取查询后结果的每一行,交给用户自己处理。读取每一行的方法(即next)其实就是封装了sqlite3_step函数。而nextWithError:主要封装了对sqlite3_step函数返回结果的处理。

    int sqlite3_step(sqlite3_stmt*);
    sqlite3_prepare函数将SQL命令字符串解析并转换为一系列的命令字节码,这些字节码最终被传送到SQlite3的虚拟数据库引擎(VDBE: Virtual Database Engine)中执行,完成这项工作的是sqlite3_step函数。比如一个SELECT查询操作,sqlite3_step函数的每次调用都会返回结果集中的其中一行,直到再没有有效数据行了。每次调用sqlite3_step函数如果返回SQLITE_ROW,代表获得了有效数据行,可以通过sqlite3_column函数提取某列的值。如果调用sqlite3_step函数返回SQLITE_DONE,则代表prepared语句已经执行到终点了,没有有效数据了。很多命令第一次调用sqlite3_step函数就会返回SQLITE_DONE,因为这些SQL命令不会返回数据。对于INSERT,UPDATE,DELETE命令,会返回它们所修改的行号——一个单行单列的值。

    // 返回YES表示从数据库中获取到了下一行数据
    - (BOOL)nextWithError:(NSError **)outErr {
        // 尝试步进到下一行
        int rc = sqlite3_step([_statement statement]);
     
        // 对返回结果rc进行处理
     
        /**
          SQLITE_BUSY 数据库文件有锁
          SQLITE_LOCKED 数据库中的某张表有锁
          SQLITE_DONE sqlite3_step()执行完毕
          SQLITE_ROW sqlite3_step()获取到下一行数据
          SQLITE_ERROR 一般用于没有特别指定错误码的错误,就是说函数在执行过程中发生了错误,但无法知道错误发生的原因。
          SQLITE_MISUSE 没有正确使用SQLite接口,比如一条语句在sqlite3_step函数执行之后,没有被重置之前,再次给其绑定参数,这时bind函数就会返回SQLITE_MISUSE。
          */
        if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
            NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [_parentDB databasePath]);
            NSLog(@"Database busy");
            if (outErr) {
                // lastError使用sqlite3_errcode获取到错误码,封装成NSError对象返回
                *outErr = [_parentDB lastError];
            }
        }
        else if (SQLITE_DONE == rc || SQLITE_ROW == rc) {
            // all is well, let's return.
        }
        else if (SQLITE_ERROR == rc) {
            // sqliteHandle就是获取到对应FMDatabase对象,然后使用sqlite3_errmsg来获取错误码的字符串
            NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
            if (outErr) {
                *outErr = [_parentDB lastError];
            }
        }
        else if (SQLITE_MISUSE == rc) {
            // uh oh.
            NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
            if (outErr) {
                if (_parentDB) {
                    *outErr = [_parentDB lastError];
                }
                else {
                    // 如果next和nextWithError函数是在当前的FMResultSet关闭之后调用的
                    // 这时输出的错误信息应该是parentDB不存在
                    NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:@"parentDB does not exist" forKey:NSLocalizedDescriptionKey];
                    *outErr = [NSError errorWithDomain:@"FMDatabase" code:SQLITE_MISUSE userInfo:errorMessage];
                }
     
            }
        }
        else {
            // wtf?
            NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
            if (outErr) {
                *outErr = [_parentDB lastError];
            }
        }
     
        // 如果不是读取下一行数据,那么就关闭数据库
        if (rc != SQLITE_ROW) {
            [self close];
        }
     
        return (rc == SQLITE_ROW);
    }
    

    [FMDatabase close]

    与open函数成对调用。主要还是封装了sqlite_close函数。

    - (BOOL)close {
        // 清除缓存的prepared语句,下面会详解
        [self clearCachedStatements];
        // 关闭所有打开的FMResultSet对象,目前看来这个_openResultSets大概也是用来调试的
        [self closeOpenResultSets];
     
        if (!_db) {
            return YES;
        }
     
        int  rc;
        BOOL retry;
        BOOL triedFinalizingOpenStatements = NO;
     
        do {
            retry   = NO;
            // 调用sqlite3_close来尝试关闭数据库
            rc      = sqlite3_close(_db);
    //如果当前数据库上锁,那么就先尝试重新关闭(置retry为YES) // 同时还尝试释放数据库中的prepared语句资源
    if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
                if (!triedFinalizingOpenStatements) {
                    triedFinalizingOpenStatements = YES;
                    sqlite3_stmt *pStmt;
    // sqlite3_next_stmt(sqlite3 *pDb, sqlite3_stmt *pStmt)表示从数据库pDb中对应的pStmt语句开始一个个往下找出相应prepared语句,如果pStmt为nil,那么就从pDb的第一个prepared语句开始。
        // 此处迭代找到数据库中所有prepared语句,释放其资源。
                    while ((pStmt = sqlite3_next_stmt(_db, nil)) !=) {
                        NSLog(@"Closing leaked statement");
                        sqlite3_finalize(pStmt);
                        retry = YES;
                    }
                }
            }
       // 关闭出错,输出错误码
            else if (SQLITE_OK != rc) {
                NSLog(@"error closing!: %d", rc);
            }
        }
        while (retry);
     
        _db = nil;
        return YES;
    }
    

    // _cachedStatements是用来缓存prepared语句的,所以清空_cachedStatements就是将每个缓存的prepared语句释放
    // 具体实现就是使用下面那个close函数,close函数中调用了sqlite_finalize函数释放资源

    - (void)clearCachedStatements {
     
        for (NSMutableSet *statements in [_cachedStatements objectEnumerator]) {
            // makeObjectsPerformSelector会并发执行同一件事,所以效率比for循环一个个执行要快很多
            [statements makeObjectsPerformSelector:@selector(close)];
        }
     
        [_cachedStatements removeAllObjects];
    }
    // 注意:此为FMResultSet的close函数
    - (void)close {
        if (_statement) {
            sqlite3_finalize(_statement);
            _statement = 0x00;
        }
     
        _inUse = NO;
    }
    
    // 清除_openResultSets
    - (void)closeOpenResultSets {
        //Copy the set so we don't get mutation errors
        NSSet *openSetCopy = FMDBReturnAutoreleased([_openResultSets copy]);
        // 迭代关闭_openResultSets中的FMResultSet对象
        for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
            FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
            // 清除FMResultSet的操作
            [rs setParentDB:nil];
            [rs close];
            [_openResultSets removeObject:rsInWrappedInATastyValueMeal];
        }
    }
    

    executeUpdate:系列函数

    注意除了“SELECT”语句外,其他的SQL语句都需要使用executeUpdate:系列函数,这些SQL语句包括CREATE, UPDATE, INSERT, ALTER, COMMIT, BEGIN, DETACH, DELETE, DROP, END, EXPLAIN, VACUUM, 和REPLACE等等。
    基本上所有executeUpdate:系列函数都是对- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]函数的封装。注意- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]函数的具体实现,基本和- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]大部分实现是差不多的,关键在于executeQuery是查询语句,所以它需要FMResultSet来保存查询的结果。而executeUpdate是非查询语句,不需要保存查询结果,但需要调用sqlite3_step(pStmt)来执行该SQL语句。这里就不赘述了,详见源码。

    executeStatements:系列函数

    使用executeStatements:函数可以将多个SQL执行语句写在一个字符串中,并执行。具体使用举例如下:

    NSString *sql = @"create table bulktest1 (id integer primary key autoincrement, x text);"
                     "create table bulktest2 (id integer primary key autoincrement, y text);"
                     "create table bulktest3 (id integer primary key autoincrement, z text);"
                     "insert into bulktest1 (x) values ('XXX');"
                     "insert into bulktest2 (y) values ('YYY');"
                     "insert into bulktest3 (z) values ('ZZZ');";
     
    success = [db executeStatements:sql];
     
    sql = @"select count(*) as count from bulktest1;"
           "select count(*) as count from bulktest2;"
           "select count(*) as count from bulktest3;";
     
    success = [self.db executeStatements:sql withResultBlock:^int(NSDictionary *dictionary) {
        NSInteger count = [dictionary[@"count"] integerValue];
        XCTAssertEqual(count, 1, @"expected one record for dictionary %@", dictionary);
        return 0;
    }];
    

    基本上executeStatements:系列函数最终封装的都是- [FMDatabase executeStatements:withResultBlock:]函数,而此函数又是对sqlite3_exec函数的封装。

    sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void *data, char **errmsg)

    该例程提供了一个执行 SQL 命令的快捷方式,SQL 命令由 sql 参数提供,可以由多个 SQL 命令组成。

    在这里,第一个参数 sqlite3 是打开的数据库对象,sqlite_callback 是一个回调,data 作为其第一个参数,errmsg 将被返回用来获取程序生成的任何错误。

    sqlite3_exec() 程序解析并执行由 sql 参数所给的每个命令,直到字符串结束或者遇到错误为止。
    executeStatements:源码如下:

    - (BOOL)executeStatements:(NSString *)sql withResultBlock:(FMDBExecuteStatementsCallbackBlock)block {
     
        int rc;
        char *errmsg = nil;
     
        rc = sqlite3_exec([self sqliteHandle], [sql UTF8String], block ? FMDBExecuteBulkSQLCallback : nil, (__bridge void *)(block), &errmsg);
     
        if (errmsg && [self logsErrors]) {
            NSLog(@"Error inserting batch: %s", errmsg);
            sqlite3_free(errmsg);
        }
     
        return (rc == SQLITE_OK);
    }
    

    executeQueryWithFormat:和executeUpdateWithFormat:函数

    考虑到如果用户直接调用printf那种形式的字符串(比如“ INSERT INTO myTable (%@) VALUES (%d)”, “age”,25),那么就需要自己将对应字符串处理成相应的SQL语句。恰好executeQuery和executeUpdate系列函数提供了相应的接口:

    - (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
    - (BOOL)executeUpdateWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
    

    其实这两个函数和其他executeQuery和executeUpdate系列方法,多的就是一个将format和…转化为可用的SQL语句步骤。其它部分其实本质还是调用- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]和- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]。下面仅列出format和…的转化代码:

    va_list args;
    // 将args指向format中第一个参数
    va_start(args, format);
     
    NSMutableString *sql      = [NSMutableString stringWithCapacity:[format length]];
    NSMutableArray *arguments = [NSMutableArray array];
     
    // 使用extractSQL函数将format和args转化为sql和arguments供后面函数使用
    [self extractSQL:format argumentsList:args intoString:sql arguments:arguments];
    // 关闭args,与va_start成对出现
    va_end(args);
    
    至于extractSQL:这个函数其实就是将(“INSERT INTO myTable (%@) VALUES (%d)”, “age”,25)中的%s和%d这种符号变成”?”,然后将”age”和25加入到arguments中。具体实现如下:
    - (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments {
     
        NSUInteger length = [sql length];
        unichar last = '\0';
        for (NSUInteger i = 0; i < length; ++i) {
            id arg = nil;
            /**            使用last和current两个变量(有些还需要next变量,比如%llu)判断当前扫描到的字符串是不是%@、
                %c、%s、%d等等。举个例子,如果碰到%s,那么说明我替换的参数其实是一个字符串,所以使用arg = 
                [NSString stringWithUTF8String:]获取到相应的arg作为参数值,
          */
            // 注意type va_arg(va_list arg_ptr,type)函数是根据传入的type参数决定返回值类型的
    
    // 另外它的作用是获取下一个参数的地址
             unichar current = [sql characterAtIndex:i];
            unichar add = current;
            if (last == '%') {
                switch (current) {
                    case '@':
                        arg = va_arg(args, id);
                        break;
                    case 'c':
                        // warning: second argument to 'va_arg' is of promotable type 'char'; this va_arg has undefined behavior because arguments will be promoted to 'int'
                        arg = [NSString stringWithFormat:@"%c", va_arg(args, int)];
                        break;
                    case 's':
                        arg = [NSString stringWithUTF8String:va_arg(args, char*)];
                        break;
                    case 'd':
                    case 'D':
                    case 'i':
                        arg = [NSNumber numberWithInt:va_arg(args, int)];
                        break;
                    case 'u':
                    case 'U':
                        arg = [NSNumber numberWithUnsignedInt:va_arg(args, unsigned int)];
                        break;
                    // %hi表示short int,%hu表示short unsigned int
                    case 'h':
                        i++;
                        if (i < length && [sql characterAtIndex:i] == 'i') {
                            //  warning: second argument to 'va_arg' is of promotable type 'short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
                            arg = [NSNumber numberWithShort:(short)(va_arg(args, int))];
                        }
                        else if (i < length && [sql characterAtIndex:i] == 'u') {
                            // warning: second argument to 'va_arg' is of promotable type 'unsigned short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
                            arg = [NSNumber numberWithUnsignedShort:(unsigned short)(va_arg(args, uint))];
                        }
                        else {
                            i--;
                        }
                        break;
                    // %qi表示long long,%qu表示unsigned long long
                    case 'q':
                        i++;
                        if (i < length && [sql characterAtIndex:i] == 'i') {
                            arg = [NSNumber numberWithLongLong:va_arg(args, long long)];
                        }
                        else if (i < length && [sql characterAtIndex:i] == 'u') {
                            arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)];
                        }
                        else {
                            i--;
                        }
                        break;
                    case 'f':
                        arg = [NSNumber numberWithDouble:va_arg(args, double)];
                        break;
                   // %g原本是根据数据选择合适的方式输出(浮点数还是科学计数法),不过此处是用float类型输出
                    case 'g':
                        // warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double'
                        arg = [NSNumber numberWithFloat:(float)(va_arg(args, double))];
                        break;
                    case 'l':
                        i++;
                        if (i < length) {
                            unichar next = [sql characterAtIndex:i];
                            if (next == 'l') {
                                i++;
                                if (i < length && [sql characterAtIndex:i] == 'd') {
                                    //%lld
                                    arg = [NSNumber numberWithLongLong:va_arg(args, long long)];
                                }
                                else if (i < length && [sql characterAtIndex:i] == 'u') {
                                    //%llu
                                    arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)];
                                }
                                else {
                                    i--;
                                }
                            }
                            else if (next == 'd') {
                                //%ld
                                arg = [NSNumber numberWithLong:va_arg(args, long)];
                            }
                            else if (next == 'u') {
                                //%lu
                                arg = [NSNumber numberWithUnsignedLong:va_arg(args, unsigned long)];
                            }
                            else {
                                i--;
                            }
                        }
                        else {
                            i--;
                        }
                        break;
                    default:
                        // something else that we can't interpret. just pass it on through like normal
                        break;
                }
            }
            else if (current == '%') {
                // 遇到%,直接跳过。
                add = '\0';
            }
            // 如果arg不为空,表示确定arg是参数,那么就使用?替换它,并将其对应参数值arg添加到arguments
            if (arg != nil) {
                [cleanedSQL appendString:@"?"];
                [arguments addObject:arg];
            }
            // 如果参数格式是%@,但此时arg是空,那么就替换为NULL
            else if (add == (unichar)'@' && last == (unichar) '%') {
                [cleanedSQL appendFormat:@"NULL"];
            }
            // 如果不是参数,就用原先字符串替换
            else if (add != '\0') {
                [cleanedSQL appendFormat:@"%C", add];
            }
            last = current;
        }
    }
    
    

    - (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt

    该函数是用来在pStmt中绑定参数值到指定(根据idx)参数上。具体封装的是sqlite3_bind系列函数。
    如果要使用sqlite3_bind
    系列函数,需要指定三个参数,一个是正在使用的sqlite_stmt对象,一个是参数索引idx,还有一个就是需要绑定的参数值,此函数解决的关键就是根据obj判断出其类型,然后调用相关的sqlite3_bind*函数,比如obj是int型,那么就调用sqlite3_bind_int函数。又或者obj是NSData类型,那么就调用sqlite_bind_blob函数。具体后面详细解释。

    - (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt {
        // 如果obj为指针为空,那么就使用sqlite3_bind_null给该参数绑定SQL null。
        if ((!obj) || ((NSNull *)obj == [NSNull null])) {
            sqlite3_bind_null(pStmt, idx);
        }
     
        // FIXME - someday check the return codes on these binds.
        else if ([obj isKindOfClass:[NSData class]]) {
            const void *bytes = [obj bytes];
            if (!bytes) {
               // 如果obj是一个空的NSData对象
                // 不要直接将NULL指针作为参数值,否则sqlite会绑定一个NULL指针给参数,而不是一个blob对象(Binary Large Object)
                bytes = "";
            }
        // SQLITE_STATIC表示传过来参数值的指针是不变的,所以完事后不需要销毁它,与其相对的是
    SQLITE_TRANSIENT
    
            sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_STATIC);
        }
        // 如果obj是一个NSDate对象
        else if ([obj isKindOfClass:[NSDate class]]) {
           // 如果你自定义了Date格式,那么就将该NSDate转化为你定义的格式,并绑定到参数上
            // 如果没有自定义Date格式,那么默认使用timeIntervalSince1970来计算参数值进行绑定
            if (self.hasDateFormatter)
                sqlite3_bind_text(pStmt, idx, [[self stringFromDate:obj] UTF8String], -1, SQLITE_STATIC);
            else
                sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]);
        }
        // 如果是NSNumber对象,注意此处判断obj类型的方法
        // @encode,@编译器指令之一,返回一个给定类型编码为一种内部表示的字符串(例如,@encode(int) → i),类似于 ANSI C 的 typeof 操作。苹果的 Objective-C 运行时库内部利用类型编码帮助加快消息分发。
        else if ([obj isKindOfClass:[NSNumber class]]) {
     
            if (strcmp([obj objCType], @encode(char)) == 0) {
                sqlite3_bind_int(pStmt, idx, [obj charValue]);
            }
            else if (strcmp([obj objCType], @encode(unsigned char)) == 0) {
                sqlite3_bind_int(pStmt, idx, [obj unsignedCharValue]);
            }
            else if (strcmp([obj objCType], @encode(short)) == 0) {
                sqlite3_bind_int(pStmt, idx, [obj shortValue]);
            }
            else if (strcmp([obj objCType], @encode(unsigned short)) == 0) {
                sqlite3_bind_int(pStmt, idx, [obj unsignedShortValue]);
            }
            else if (strcmp([obj objCType], @encode(int)) == 0) {
                sqlite3_bind_int(pStmt, idx, [obj intValue]);
            }
            else if (strcmp([obj objCType], @encode(unsigned int)) == 0) {
                sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedIntValue]);
            }
            else if (strcmp([obj objCType], @encode(long)) == 0) {
                sqlite3_bind_int64(pStmt, idx, [obj longValue]);
            }
            else if (strcmp([obj objCType], @encode(unsigned long)) == 0) {
                sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongValue]);
            }
            else if (strcmp([obj objCType], @encode(long long)) == 0) {
                sqlite3_bind_int64(pStmt, idx, [obj longLongValue]);
            }
            else if (strcmp([obj objCType], @encode(unsigned long long)) == 0) {
                sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongLongValue]);
            }
            else if (strcmp([obj objCType], @encode(float)) == 0) {
                sqlite3_bind_double(pStmt, idx, [obj floatValue]);
            }
            else if (strcmp([obj objCType], @encode(double)) == 0) {
                sqlite3_bind_double(pStmt, idx, [obj doubleValue]);
            }
            else if (strcmp([obj objCType], @encode(BOOL)) == 0) { // bool使用sqlite3_bind_int来绑定的
                sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0));
            }
            else {
                sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
            }
        }
        else {
            sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
        }
    }
    
    

    openWithFlags:系列函数

    除了前面提到过的open函数外,FMDB还为我们提供了openWithFlags:系列函数,其本质是封装了sqlite3_open_v2。

    int sqlite3_open_v2(
      const char *filename,   /* 数据库名称 (UTF-8) */
      sqlite3 **ppDb,         /* 输出: SQLite数据库对象 */
      int flags,              /* 标识符 */
      const char *zVfs        /* 想要使用的VFS名称 */
    )
    
    对于sqlite3_open和sqlite3_open16函数,如果可能将以可读可写的方式打开数据库,否则以只读的方式打开数据库。如果要打开的数据库文件不存在,就新建一个。对于sqlite3_open_v2函数,情况就要复杂一些了,因为这个v2版本的函数强大就强大在它可以对打开(连接)数据库的方式进行控制,具体是通过它的参数flags来完成。sqlite3_open_v2函数只支持UTF-8编码的SQlite3数据库文件。
    
    如flags设置为SQLITE_OPEN_READONLY,则SQlite3数据库文件以只读的方式打开,如果该数据库文件不存在,则sqlite3_open_v2函数执行失败,返回一个error。如果flags设置为SQLITE_OPEN_READWRITE,则SQlite3数据库文件以可读可写的方式打开,如果该数据库文件本身被操作系统设置为写保护状态,则以只读的方式打开。如果该数据库文件不存在,则sqlite3_open_v2函数执行失败,返回一个error。如果flags设置为SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,则SQlite3数据库文件以可读可写的方式打开,如果该数据库文件不存在则新建一个。这也是sqlite3_open和sqlite3_open16函数的默认行为。除此之外,flags还可以设置为其他标志,具体可以查看SQlite官方文档。
    
    参数zVfs允许客户应用程序命名一个虚拟文件系统(Virtual File System)模块,用来与数据库连接。VFS作为SQlite library和底层存储系统(如某个文件系统)之间的一个抽象层,通常客户应用程序可以简单的给该参数传递一个NULL指针,以使用默认的VFS模块。
    
    对于UTF-8编码的SQlite3数据库文件,推荐使用sqlite3_open_v2函数进行连接,它可以对数据库文件的打开和处理操作进行更多的控制。
    

    FMResultSet其他的获取结果方式

    FMResultSet的resultSetWithStatement:、close、next函数。其实FMResultSet除了使用next获取查询结果外,还有很多其他的接口可以查询到结果。
    一系列的ForColumn:和ForColumnIndex:(表示对应的数据类型)函数都是用来获取查询结果的。这里值得注意的是ForColumn:函数本质是调用相应的*ForColumnIndex:函数。比如:

    - (int)intForColumn:(NSString*)columnName {
        return [self intForColumnIndex:[self columnIndexForName:columnName]];
    }
    
    上述函数实现内部做了一个转化,就是利用columIndexForName:函数查询到这个columnName对应的索引值。而这个columnIndexForName:本质是根据_columnNameToIndexMap属性获取到列名称(columnName)的对应列号(columnIdx)。_columnNameToIndexMap是一个NSMutableDictionary对象。其中key表示的是指定结果集中对应列的名称,value表示的是指定结果集中对应的列号(columnIdx)。所以我们这里主要看下columnNameToIndexMap的实现:
    
    - (NSMutableDictionary *)columnNameToIndexMap {
        if (!_columnNameToIndexMap) {
            // 找出由statement指定的结果集中列的数目
            int columnCount = sqlite3_column_count([_statement statement]);
            _columnNameToIndexMap = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount];
            int columnIdx = 0;
            // 将列号和该列对应名称绑定在一起,组成_columnNameToIndexMap
            for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
                [_columnNameToIndexMap setObject:[NSNumber numberWithInt:columnIdx]
                                          forKey:[[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)] lowercaseString]];
            }
        }
        return _columnNameToIndexMap;
    }
    
    这时我们再回头看看*ForColumnIndex:函数的实现。它的本质就是调用sqlite3_column_*(*表示对应的数据类型),也就是从statement中获取到对应列号的数据,比如
    - (int)intForColumnIndex:(int)columnIdx {
        return sqlite3_column_int([_statement statement], columnIdx);
    }
    

    FMDB的加解密

    FMDB中使用- [FMDatabase setKey:]和- [FMDatabase setKeyWithData:]输入数据库密码以求验证用户身份,使用- [FMDatabase rekey:]和- [FMDatabase rekeyWithData:]来给数据库设置密码或者清除密码。这两类函数分别对sqlite3_key和sqlite3_rekey函数进行了封装。
    int sqlite3_key( sqlite3 *db, const void *pKey, int nKey)

    db 是指定数据库,pKey 是密钥,nKey 是密钥长度。例:sqlite3_key( db, "abc", 3);
    sqlite3_key是输入密钥,如果数据库已加密必须先执行此函数并输入正确密钥才能进行操作,如果数据库没有加密,执行此函数后进行数据库操作反而会出现“此数据库已加密或不是一个数据库文件”的错误。

    int sqlite3_rekey( sqlite3 *db, const void *pKey, int nKey)
    参数同sqlite3_key。
    sqlite3_rekey是变更密钥或给没有加密的数据库添加密钥或清空密钥,变更密钥或清空密钥前必须先正确执行 sqlite3_key。在正确执行 sqlite3_rekey 之后在 sqlite3_close 关闭数据库之前可以正常操作数据库,不需要再执行 sqlite3_key。
    清空密钥为 sqlite3_rekey( db, NULL, 0)。

    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 databaseQueueWithPath:]

    // 调用initWithPath:函数构建一个FMDatabaseQueue对象

    + (instancetype)databaseQueueWithPath:(NSString*)aPath {
        FMDatabaseQueue *q = [[self alloc] initWithPath:aPath];
        FMDBAutorelease(q);
        return q;
    }
    
    // 使用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;
    }
    
    

    [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具体是怎么完成多线程的:


    image.png

    [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;
    }
    
    

    [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
    }
    

    总结

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

    相关文章

      网友评论

          本文标题:有关FMDB的理解以及源码分析

          本文链接:https://www.haomeiwen.com/subject/ypuknktx.html