【原】FMDB源碼閱讀(三)
本文轉載請注明出處 —— polobymulberry-博客園
1. 前言
FMDB比較優秀的地方就在於對多線程的處理。所以這一篇主要是研究FMDB的多線程處理的實現。而FMDB最新的版本中主要是通過使用FMDatabaseQueue這個類來進行多線程處理的。
2. FMDatabaseQueue使用舉例
// 創建,最好放在一個單例的類中 FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath]; // 使用 [queueinDatabase
:^(FMDatabase *db) { [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]]; [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]]; [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]]; FMResultSet *rs = [db executeQuery:@"select * from foo"]; while ([rs next]) { // … } }]; // 如果要支持事務 [queueinTransaction
:^(FMDatabase *db, BOOL *rollback) { [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]]; [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]]; [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]]; if (whoopsSomethingWrongHappened) { *rollback = YES; return; } // etc… [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:4]]; }];
我們可以看到FMDB的多線程實現主要是依賴於FMDatabaseQueue這個類。下面我們結合上面這個例子,來具體看看FMDatabaseQueue的內部實現。
2.1 + [FMDatabaseQueue databaseQueueWithPath:]
// 調用initWithPath:函數構建一個FMDatabaseQueue對象 + (instancetype)databaseQueueWithPath:(NSString*)aPath { FMDatabaseQueue *q = [[self alloc] initWithPath:aPath]; FMDBAutorelease(q); return q; }
查看initWithPath:函數,發現其本質是調用 - (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName函數。
// 使用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; }
2.2 – [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具體是怎么完成多線程的:
2.3 – [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; }
2.4 – [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 = 0; __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:0 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 }
3. FMDatabasePool(建議使用FMDatabaseQueue)
Tip:
除非你真的知道在什么情況下(比如所有操作均為讀操作)可以使用FMDatabasePool,否則盡量改用FMDatabaseQueue,不然可能會引起死鎖。
4. 總結
FMDB比較常用的幾個類基本上學習完畢。FMDB代碼上不是很難,核心還是SQLite3和數據庫的知識。更重要的還是要知道真實環境中的最佳實踐。