【原】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具體是怎么完成多線程的:

image

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和數據庫的知識。更重要的還是要知道真實環境中的最佳實踐。

5. 參考文獻



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM