【原】FMDB源碼閱讀(三),fmdb源碼閱讀
【原】FMDB源碼閱讀(三)
本文轉載請註明出處 —— polobymulberry-部落格園
1. 前言
FMDB比較優秀的地方就在於對多線程的處理。所以這一篇主要是研究FMDB的多執行緒的實現。而FMDB最新的版本中主要是通過使用FMDatabaseQueue這個類來進行多執行緒的。
2. FMDatabaseQueue使用舉例
// 建立,最好放在一個單例的類中FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];// 使用[queue
inDatabase
:^(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]) { // … }}];// 如果要支援事務[queue
inTransaction
:^(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具體是怎麼完成多線程的:
- (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和資料庫的知識。更重要的還是要知道真實環境中的最佳實務。
5. 參考文獻
- 53sqlite的事務和鎖
SQLite 事務(Transaction)