【原】FMDB源碼閱讀(三),fmdb源碼閱讀

來源:互聯網
上載者:User

【原】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)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.