【原】FMDB源碼閱讀(一) 本文轉載請註明出處 —— polobymulberry-博客園 1. 前言 說實話,之前的SDWebImage和AFNetworking這兩個組件我還是使用過的,但是對於FMDB組件我是一點都沒用過。好在FMDB源碼中的main.m文件提供了大量的示例,況且網上也有很多
【原】FMDB源碼閱讀(一)
本文轉載請註明出處 —— polobymulberry-博客園
1. 前言
說實話,之前的SDWebImage和AFNetworking這兩個組件我還是使用過的,但是對於FMDB組件我是一點都沒用過。好在FMDB源碼中的main.m文件提供了大量的示例,況且網上也有很多最佳實踐的例子,我就不在這獻醜了。我們先從一個最簡單的FMDB的例子開始:
// 找到用戶目錄下的Documents文件夾位置NSString* docsdir = [NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
// 將user.sqlite放到Documents文件夾下,並生成user.sqlite的絕對路徑 NSString* dbpath = [docsdir stringByAppendingPathComponent:@"user.sqlite"];
// 根據user.sqlite的絕對路徑獲取到一個FMDatabase對象,其實就是一個封裝了的SQLite資料庫對象 FMDatabase* db = [FMDatabase databaseWithPath:dbpath];
// 打開該資料庫
[db open];
// 執行SQL語句 - select * from people
FMResultSet *rs = [db
executeQuery
:@"select * from people"];
// 利用next函數,迴圈輸出結果 while ([rs next]) { NSLog(@"%@ %@", [rs stringForColumn:@"firstname"], [rs stringForColumn:@"lastname"]); }
// 關閉該資料庫
[db close];
很簡單是吧,甚至我覺得上面我寫的註釋都多餘了。確實,FMDB說白了就是對SQLite資料庫的C/C++介面進行了一層封裝,當然功能也更為強大,比如多線程操作,另外FMDB介面要比原生的SQLite介面簡潔很多。下麵我們就上面的例子研究下FMDB的基本流程。
2. FMDB的最基本流程(結合上面例子)
我們先看看上面代碼中我用藍色粗體高亮的部分,研究下其具體實現。
2.1 + [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 = 2; } return self; }
2.2 - [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 > 0) { 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會不斷調用回調函數,直到其他進程或者線程釋放鎖。獲得鎖之後,不會再調用回調函數,從而向下執行,進行資料庫操作。該函數是在獲取不到鎖的時候,以執行回調函數的次數來進行延遲,等待其他進程或者線程操作資料庫結束,從而獲得鎖操作資料庫。
大家也看出來了,sqlite3_busy_handler函數的關鍵就是這個回調函數了,此處作者定義的是一個名叫FMDBDatabaseBusyHandler的函數作為其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 == 0) { self->_startBusyRetryTime = [NSDate timeIntervalSinceReferenceDate]; return 1; } // 使用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(50) + 50; 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 1; } return 0; }
2.3 - [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:](重點)
為什麼不講 - [FMDatabase executeQuery:]?因為- [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], -1, &pStmt, 0); // 如果生成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 = 0; // 獲取到pStmt中需要綁定的參數個數 int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!) // 舉一個使用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" */
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 > 0) { [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] + 1]; FMDBRelease(statement); // 生成statement的操作已經結束 _isExecutingStatement = NO; return rs; }
2.4 - [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); }
2.5 - [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)) !=0) { 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]; } }
3. 總結
本文結合一個基本的FMDB使用案例,介紹了FMDB基本的運作流程和內部實現。總的來說,FMDB就是對SQLite的封裝,所以學習FMDB根本還是在學習SQLite資料庫操作。
4. 參考文章
-
SQlite資料庫的C編程介面(三) 預處理語句(Prepared Statements) ——《Using SQlite》讀書筆記 .
-
SQlite資料庫的C編程介面(六) 返回值和錯誤碼(Result Codes and Error Codes) ——《Using SQlite》讀書筆記