FMDB源码解读

iOS App开发中数据持久层一般用的是FMDB,FMDB是对SQLite的一层Wrapper,封装了比较难懂的C接口,提供了面向Objective-C的接口方法,对iOS开发者很友好。

正如FMDB源码注释中所说的那样FMDB主要有3个类:

  • FMDatabase

    SQLite数据库,用于执行SQL语句。

  • FMResultSet

    在FMDatabase对象上执行查询语句得到的结果集

  • FMDatabaseQueue

    用于多线程数据库在的查询、更新

FMDatabase

FMDatabase描述的是SQLite数据库,它负责管理数据库的创建、打开、关闭、事物、CRUD语句执行。

创建数据库

创建数据库时只需指定数据库文件路径即可,具体路径可为以下3种之一:

  1. 文件系统路径,这个文件可以不存在,如果文件不存在就创建。
  2. 一个空字符串@“”,此时会在临时目录下创建一个空数据库,这个数据库会在数据库链接关闭时删除。
  3. nil,会创建一个内存数据库,这个数据库会在数据库链接关闭时销毁。

FMDatabase提供了2个方法供使用者创建数据库,一个是类方法,另一个是实例方法,类方法最终调用的也是实例方法,默认初始化方法中主要初始化了数据库路径、查询结果集合、最大忙重试时间。

打开数据库

FMDB提供了3个打开数据库的方法,其中open方法调用的是sqlite3_open()函数,-(BOOL)openWithFlags:(int)flags(BOOL)openWithFlags:(int)flags vfs:(NSString *)vfsName调用的是sqlite3_open_v2函数,其中sqlite3_open_v2函数可设置打开模式及虚拟文件系统名称,其中打开模式有SQLITE_OPEN_READONLYSQLITE_OPEN_READWRITESQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,数据库打开后设置忙重试回调sqlite3_busy_handler,回调方法如果返回0就不在重试。

CRUD

在FMDB中把CRUD分为了2种操作:更新(没有结果返回,比如UPDATE、INSERT、DELETE)、查询(有结果返回,比如QUERY)

  • 更新操作

FMDB中所有的更新方法最终调用的都是(BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args方法,该方法调用了SQLite的3个C接口API,分别为:

  1. sqlite3_prepare_v2

    为了方便SQLite处理SQL语句,需要先把字符串预处理成sqlite3_stmt

  2. sqlite3_bind

    把参数绑定到SQLite中?预置符位置

  3. sqlite_step

    执行SQLite更新语句

在执行更新语句中为了提高效率(sqlite3_prepare_v2很耗时),增加了sqlite3_stmt预处理语句缓存机制,如果使能了_shouldCacheStatements属性,那么经过预处理的SQL语句会被缓存起来,等下次再使用同样的SQL语句时执行从缓存中获取,获取到后调用sqlite3_reset函数就可以继续绑定新的参数。

绑定参数主要是根据参数的数据类型调用相关的bind函数,如何判断参数是何种类型?根据objCType方法返回的类型字符串判断,例如:

1
if (strcmp([obj objCType], @encode(int)) == 0) {
            sqlite3_bind_int(pStmt, idx, [obj intValue]);
 }

如果参数是NSData类型,那么调用sqlite3_bind_blob,如果是int类型,调用sqlite3_bind_int,FMDB中除了NSString, NSNumber, NSNull, NSDate, 和 NSData之外其它对象类型均被认为是text类型,调用对象的description方法,具体参考源码中(void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt方法。

  • 查询操作

查询操作与上述更新操作有部分类似,也会调用sqlite3_prepare_v2sqlite3_bind接口,但是查询操作不会立即执行sqlite_step,只有在FMResultSet上调用next方法时才会执行,查询操作会返回FMResultSet对象。

关闭数据库

关闭数据库主要是释放申请的相关资源,包括清空缓存语句、关闭查询结果、关闭数据库(用的是sqlite3_close)。

事物

当想要原子操作数据库时可使用事物,FMDB提供了立即执行和延迟执行的事物,SQL语句分别为begin exclusive transactionbegin deferred transaction,当想要提交事物时执行commit transaction语句,当想要回滚时执行rollback transaction语句,上述代码逻辑具体参考源码中以下方法:

1
- (BOOL)beginTransaction;
- (BOOL)beginDeferredTransaction;
- (BOOL)commit;
- (BOOL)rollback;

FMResultSet

FMResultSet是查询结果集,对外主要提供了2大类查询方法,一类是根据列索引查(xxxForColumnIndex,xxx表示的是具体类型),另一类是根据列名称查询(xxxForColumn,xxx表示的是具体类型)。实际上根据列名称查询方法最终调用的是根据列索引查询的方法,大概的实现逻辑为:FMResultSet内部维护了一个列名到列索引的映射表,当调用列名称时先通过列名到列索引的映射表查到相应的列索引号,然后调用列索引查询方法(列索引方法调用的是sqlite3_column_xx),那列名到列索引的映射表是怎么构造的呢?先根据sqlite3_column_count获取到列数,然后遍历每一列,再根据sqlite3_column_name获取列名即可完成映射,具体代码如下:

1
- (NSMutableDictionary *)columnNameToIndexMap {
    if (!_columnNameToIndexMap) {
        int columnCount = sqlite3_column_count([_statement statement]);
        _columnNameToIndexMap = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount];
        int columnIdx = 0;
        for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
            [_columnNameToIndexMap setObject:[NSNumber numberWithInt:columnIdx]
                                      forKey:[[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)] lowercaseString]];
        }
    }
    return _columnNameToIndexMap;
}

FMDatabaseQueue

FMDatabase源码中写的很清楚,在多线程中用一个<FMDatabase>实例是坏想法,<FMDatabase>不是线程安全的,想要在多线程上操作数据库就用FMDatabaseQueue,FMDatabaseQueue会在内部的一个串行对列上执行blocks代码块,因此即便你在同一时间多个线程上调用FMDatabaseQueue方法,这些查询和更新操作也不会混乱,它们会根据顺讯依次执行。

FMDatabaseQueue主要功能就是提供了一个内部串行对列,确保了多线程安全,具体的数据库操作逻辑还是用的FMDatabase,FMDatabaseQueue在执行blocks时是同步执行的,所以有可能引发死锁,为了解决死锁FMDatabase在创建内部对列时通过dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);方法设置了一个context,当在执行block时先判断当前Queue的context,如果和之前设置的context相等则发生死锁,通过assert提示使用者发生死锁,例如如下代码就会发生死锁:

1
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:[NSTemporaryDirectory() stringByAppendingString:@"temp.db"]];
    [queue inDatabase:^(FMDatabase *db) {
        NSLog(@"in queue");
      //在同一个串行对列中同步执行,故而发生死锁
        [queue inDatabase:^(FMDatabase *db) {
            NSLog(@"in nested queue");
        }];
    }]

切记不要在FMDatabase的block中嵌套调用。