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

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和数据库的知识。更重要的还是要知道真实环境中的最佳实践。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容