本文轉載請註明出處 —— polobymulberry-博客園html
說實話,以前的SDWebImage和AFNetworking這兩個組件我仍是使用過的,可是對於FMDB組件我是一點都沒用過。好在FMDB源碼中的main.m文件提供了大量的示例,何況網上也有不少最佳實踐的例子,我就不在這獻醜了。咱們先從一個最簡單的FMDB的例子開始:sql
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的基本流程。數據庫
咱們先看看上面代碼中我用藍色粗體高亮的部分,研究下其具體實現。編程
// 核心其實仍是調用了+[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; }
上面提到過+ [FMDatabase databaseWithPath:]和- [FMDatabase initWithPath:]本質上只是給了數據庫一個名字,並無真實建立或者獲取數據庫。這裏的open函數纔是真正獲取到數據庫,其本質上也就是調用SQLite的C/C++接口 – sqlite3_open()。緩存
sqlite3_open(const char *filename, sqlite3 **ppDb)安全
該例程打開一個指向 SQLite 數據庫文件的鏈接,返回一個用於其餘 SQLite 程序的數據庫鏈接對象。session
若是 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; }
爲何不講 - [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; }
- [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); }
與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]; } }
本文結合一個基本的FMDB使用案例,介紹了FMDB基本的運做流程和內部實現。總的來講,FMDB就是對SQLite的封裝,因此學習FMDB根本仍是在學習SQLite數據庫操做。