FMDB 源碼解析 FMDatabase類

前言

在上篇文章中介紹了文件的組成並詳細的介紹了 FMResultSet 類,本文將接着上篇的分析進行 FMDatabase 文件的解讀。php

文件組成

FMDB源碼主要有一下幾個文件組成:html

FMDatabase:表示一個單獨的SQLite DB實例,經過它能夠對數據庫進行增刪改查等操做。sql

FMResultSet:表示經過sql在DB中查詢到的結果集,而且將查詢結果轉化成對應的值或對象,例如:int、long、bool、NSString、NSDate、NSData、char *、 id等。數據庫

FMDatabaseQueue:用來管理數據查詢的隊列,保證大部分時間下對數據庫的操做是串行的。數組

FMDatabaseAdditions:做爲 FMDatabase類的拓展。新增了一些經常使用的校驗方法,例如:表是否存在、列是否存在、版本號、sql校驗等。緩存

FMDatabasePool: 用來管理數據庫查詢任務。不過在頭文件中,做者寫的很是清楚牆裂不建議使用,而是用 FMDatabaseQueue代替。若是必定要用的話,必定要注意死鎖。bash

FMDatabase

  • 表示單個SQLite數據庫。用於執行SQL語句。
  • 不要實例化單個FMDatabase對象並在多個線程中使用它。用 FMDatabaseQueue 代替。
  • 本文中會對重點方法詳細解析,其餘的方法一掠而過。

方法包含

  1. + (NSString*)FMDBUserVersion; FMDB版本
  2. + (NSString*)sqliteLibVersion;sqliteLib版本號
  3. - (BOOL)open 打開數據庫,並返回狀態標識

打開數據庫

- (BOOL)open {
    if (_db) {
        return YES;
    }
    
    int err = sqlite3_open([self sqlitePath], &_db );
    if(err != SQLITE_OK) {
        NSLog(@"error opening!: %d", err);
        return NO;
    }
    
    if (_maxBusyRetryTimeInterval > 0.0) {
        // set the handler
        [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
    }
    return YES;
}

複製代碼
  • 判斷當前db是否存在,存在則直接返回狀態0
  • 不存在的話,調用底層的 sqlite3_open方法,傳入兩個參數,數據庫的localPath和db的內存地址,而且返回執行的狀態結果。
  • 若是狀態是 SQLITE_OK則繼續向下執行。
  • maxBusyRetryTimeInterval 初始化的時候默認設置爲2。
static int FMDBDatabaseBusyHandler(void *f, int count) {
    FMDatabase *self = (__bridge FMDatabase*)f;
    
    if (count == 0) {
        self->_startBusyRetryTime = [NSDate timeIntervalSinceReferenceDate];
        return 1;
    }
    
    NSTimeInterval delta = [NSDate timeIntervalSinceReferenceDate] - (self->_startBusyRetryTime);
    
    if (delta < [self maxBusyRetryTimeInterval]) {
        sqlite3_sleep(50); // milliseconds
        return 1;
    }
    
	return 0;
}

- (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeout {
    
    _maxBusyRetryTimeInterval = timeout;
    
    if (!_db) {
        return;
    }
    
    if (timeout > 0) {
        sqlite3_busy_handler(_db, &FMDBDatabaseBusyHandler, (__bridge void *)(self));
    }
    else {
        // turn it off otherwise
        sqlite3_busy_handler(_db, nil, nil);
    }
}
複製代碼
  • FMDBDatabaseBusyHandler 註冊一個回調來處理SQLITE_BUSY錯誤
  • 它的sqlite3_busy_handler(D,X,P)例程設置了一個回調函數X,當另外一個線程或進程將表鎖定時,只要嘗試訪問與[database connection] D關聯的數據庫表,就能夠用參數P調用它。 sqlite3_busy_handler()接口用於實現[sqlite3_busy_timeout()][PRAGMA busy_timeout]。
  • 若是 busy callBackNULL,則遇到鎖后里面返回 SQLITE_BUSY。若是 busy callBack 不是 NULL,則可使用兩個參數做爲回調。
  • busy handler 的第一個參數是 void * 指針的副本,同時他也是 sqlite3_busy_handler()的第三個參數。
  • sqlite3_busy_handler 的第二個參數是須要回調的 busy handler 的次數,表明前面相同 locking event的次數
  • 若是 busy callback 返回0,則不會進行其餘嘗試來訪問數據庫,直接返回 SQLITE_BUSY ,若是不是0,則再次嘗試訪問數據庫並重復循環。
  • busy handler並不能確保有 在lock contention 的時候被調用。若是 SQLite 斷定在調用 busy handler 的時候會形成死鎖,則會直接返回 SQLITE_BUSY,而再也不調用 busy handler
  • 考慮到一個場景,其中一個線程持有一個 read lock嘗試提高爲 reserved lock,另外一個線程持有一個 reserved lock 嘗試提高爲 exclusive lock。這個時候,第一個線程沒法進行,由於它被第二個 blocked;第二個也沒有辦法進行,由於它被第一個blocked。若是兩個線程都調用了 busy handlers,則二者都不會成功。所以,SQLite爲第一個線程返回 SQLITE_BUSY,但願第一個線程釋放其 read lock,而且第二個線程能夠繼續。
  • callback 的默認值是NULL
  • 每一個 [database connection] 只能設置一個 busy handler.設置新的 handler 的時候,須要提早清除以前的 handler。注意:調用 [sqlite3_busy_timeout()] 或者計算 [PRAGMA busy_timeout=N]將會改變 busy handler 從而清除以前的設置。
  • busy callback 不該執行任何修改調用 busy handler的數據庫鏈接操做。換句話說,busy handler 是不容許重入的。任何此類操做都會致使未定義的行爲。
  • busy handler 不能關閉數據庫鏈接,也不能調用 [prepared statement] 方法。

執行語句

- (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;
    FMStatement *statement  = 0x00;
    FMResultSet *rs         = 0x00;
    
    if (_traceExecution && sql) {
        NSLog(@"%@ executeQuery: %@", self, sql);
    }
    
    if (_shouldCacheStatements) {
        statement = [self cachedStatementForQuery:sql];
        pStmt = statement ? [statement statement] : 0x00;
        [statement reset];
    }
    
    if (!pStmt) {
    
        rc      = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);
        
        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();
            }
            
            sqlite3_finalize(pStmt);
            _isExecutingStatement = NO;
            return nil;
        }
    }
    
    id obj;
    int idx = 0;
    int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)
    
    // If dictionaryArgs is passed in, that means we are using sqlite's named parameter support if (dictionaryArgs) { for (NSString *dictionaryKey in [dictionaryArgs allKeys]) { // Prefix the key with a colon. NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey]; if (_traceExecution) { NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]); } // Get the index for the parameter name. int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]); FMDBRelease(parameterName); if (namedIdx > 0) { // Standard binding from here. [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt]; // increment the binding count, so our check below works out idx++; } else { NSLog(@"Could not find index for %@", dictionaryKey); } } } else { while (idx < queryCount) { if (arrayArgs && idx < (int)[arrayArgs count]) { obj = [arrayArgs objectAtIndex:(NSUInteger)idx]; } else if (args) { obj = va_arg(args, id); } else { //We ran out of arguments 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 if (!statement) { statement = [[FMStatement alloc] init]; [statement setStatement:pStmt]; if (_shouldCacheStatements && sql) { [self setCachedStatement:statement forQuery:sql]; } } // the statement gets closed in rs's dealloc or [rs close];
    rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
    [rs setQuery:sql];
    
    NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
    [_openResultSets addObject:openResultSet];
    
    [statement setUseCount:[statement useCount] + 1];
    
    FMDBRelease(statement); 
    
    _isExecutingStatement = NO;
    
    return rs;
}

複製代碼
  • 執行sql 成功的話會返回 FMResultSet對象,失敗的話返回 nil;和執行更新語句同樣,有一個變量接收error對象。你能夠用 lastErrorMessagelastErrorMessage 方法來肯定查詢失敗的緣由。
  • 爲了迭代查詢結果,一般狀況下會使用「while()」循環。經過<[FMResultSet next]>來實現從一個記錄到另外一個記錄切換。
  • 這個方法使用 sqlite3_bind可選的參數值(sqlite.org/c3ref/bind_… )。能夠正確地轉義任何須要轉義序列的字符(例如引號),從而消除簡單的SQL錯誤並防止SQL注入攻擊。本地處理 nsstringnsnumber、「nsnull」、「nsdate」和「nsdata」對象。全部其餘對象類型將使用對象的「description」方法解釋爲文本值。
  • sql 參數,SELECT statement 可使用 ?來佔位。
  • 可選參數中的 只能是OC對象(例如 nsstringnsnumber 等),而不是基本的c數據類型(例如「int」、「char」等)。
  • 判斷數據庫是否存在,不存在返回 0x00(nil)
  • 判斷是否是在執行 statement,在執行的話,提示數據庫正在使用,並返回 0x00
  • 而後將 isExecutingStatement置爲 yes,開始進行下面的處理。
  • 根據 shouldCacheStatements 字段來判斷是否是緩存傳入的 Statements ,緩存了的話,經過sql做爲key取出 statementsSets,若是取出的對象是 preparestatement 則賦值給 sqlite3_stmt
  • 若是pStmt 不存在,則調用 sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0); 返回成功或失敗狀態,並將準備好的 statement 值放到 pStmt中。
  • int sqlite3_bind_parameter_count(sqlite3_stmt*) 返回 [SQL parameters]參數的個數
  • dictionaryArgs 遍歷裏面的參數名的值,經過該值拿到name對應的index
  • [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt] 將傳入的參數和前面sql的 值綁定
  • idx 用來遍歷參數的個數,若是沒有傳入 dictionaryArgs 參數,傳入的而是 arrayArgs ,則經過遍歷數組的拿到對應的 obj 綁定到 idx位置,即:將通配符?:age按照索引 賦值爲 obj
  • 若是數組中的參數個數,和 pStmt 中參數的個數不一樣,則拋出錯誤
  • 將參數和 綁定完的 pStmt 賦值給 FMStatement,若是須要緩存,則將 sql 做爲 keyFMStatement 做爲 object 對象放到緩存的字典裏面
  • 後面就是將 statement 賦值給 FMResultSet 執行操做。做者特地提到 在 rs的 dealloc 或者 [rs close]會將statement close

更新

- (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {

...
綁定完數據,生成最終的 pStmt
   rc      = sqlite3_step(pStmt);

...
}
複製代碼
  • 前面的步驟和execute都是一致的,只有獲取rc的方式不同,sqlite3_step
拓展:
sqlite3_step()

這個過程用於執行有前面sqlite3_prepare建立的準備語句。這個語句執行到結果的第一行可用的位置。繼續前進到結果的第二行的話,只需再次調用sqlite3_step()。繼續調用sqlite3_setp()知道這個語句完成,那些不返回結果的語句(如:INSERT,UPDATE,或DELETE),sqlite3_step()只執行一次就返回

函數的返回值基於建立sqlite3_stmt參數所使用的函數,假如是使用老版本的接口sqlite3_prepare()和sqlite3_prepare16(),返回值會是 SQLITE_BUSY, SQLITE_DONE, SQLITE_ROW, SQLITE_ERROR 或 SQLITE_MISUSE,而v2版本的接口sqlite3_prepare_v2()和sqlite3_prepare16_v2()則會同時返回這些結果碼和擴展結果碼。
對全部V3.6.23.1以及其前面的全部版本,須要在sqlite3_step()以後調用sqlite3_reset(),在後續的sqlite3_ step以前。若是調用sqlite3_reset重置準備語句失敗,將會致使sqlite3_ step返回SQLITE_MISUSE,可是在V3. 6.23.1之後,sqlite3_step()將會自動調用sqlite3_reset。

複製代碼
  • 每調用一次 [cachedStmt useCount] + 1

轉換

- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... {
    va_list args;
    va_start(args, format);
    NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]];
    NSMutableArray *arguments = [NSMutableArray array];
    [self extractSQL:format argumentsList:args intoString:sql arguments:arguments];
    va_end(args);
    return [self executeQuery:sql withArgumentsInArray:arguments];
}
複製代碼

該方法其實調用的是- (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments,其中 sqlSELECT * FROM t_student WHERE age > %dcleanedSQL 爲轉換完的值 SELECT * FROM t_student WHERE age > ?markdown

加解密

  • 加密
- (BOOL)setKey:(NSString*)key;

- (BOOL)setKeyWithData:(NSData *)keyData {
#ifdef SQLITE_HAS_CODEC
    if (!keyData) {
        return NO;
    }
    
    int rc = sqlite3_key(_db, [keyData bytes], (int)[keyData length]);
    
    return (rc == SQLITE_OK);
#else
#pragma unused(keyData)
    return NO;
#endif
}
複製代碼
  • 解密
- (BOOL)rekey:(NSString*)key;
- (BOOL)rekeyWithData:(NSData *)keyData {
#ifdef SQLITE_HAS_CODEC
    if (!keyData) {
        return NO;
    }
    
    int rc = sqlite3_rekey(_db, [keyData bytes], (int)[keyData length]);
    
    if (rc != SQLITE_OK) {
        NSLog(@"error on rekey: %d", rc);
        NSLog(@"%@", [self lastErrorMessage]);
    }
    
    return (rc == SQLITE_OK);
#else
#pragma unused(keyData)
    return NO;
#endif
}
複製代碼

FMDatabaseAdditions

該類做爲 FMDatabase 的補充,添加了一些經常使用的方法app

-(BOOL)validateSQL:(NSString)sql error:(NSError*)error;sql的有效性函數

-(BOOL)tableExists:(NSString*)tableName;數據庫表是否存在。

-(BOOL)columnExists:(NSString)columnName inTableWithName:(NSString)tableName;在tableName表中columnName是否存在。

-(FMResultSet*)getSchema;數據庫的一些概要信息

寫在最後

1.歡迎你們對文章給出建議或意見。

2.本文凝結了做者的心血,但願你們在轉發、傳閱的時候可以保留文章的初始地址。

相關連接:

1.FMDB 源碼解析 FMResultSet類

2.FMDB 源碼解析 FMDatabase類

參考連接: 1.www.sqlite.org/index.html

  1. www.code4app.com/home.php?mo…

  2. www.jianshu.com/p/967a213a4…