iOS開發原始碼閱讀篇--FMDB原始碼分析3(FMDatabaseQueue+FMDatabasePool)

邏輯教育-楚陽發表於2018-12-03

一、前言

如上一章所講,FMDB原始碼主要有以下幾個檔案組成:

FMResultSet : 表示FMDatabase執行查詢之後的結果集。

FMDatabase : 表示一個單獨的SQLite資料庫操作例項,通過它可以對資料庫進行增刪改查等等操作。

FMDatabaseAdditions : 擴充套件FMDatabase類,新增對查詢結果只返回單個值的方法進行簡化,對錶、列是否存在,版本號,校驗SQL等等功能。

FMDatabaseQueue : 使用序列佇列 ,對多執行緒的操作進行了支援。

FMDatabasePool : 使用任務池的形式,對多執行緒的操作提供支援。(不過官方對這種方式並不推薦使用,優先選擇FMDatabaseQueue的方式:ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD)

FMDB比較優秀的地方就在於對多執行緒的處理。所以這一篇主要是研究FMDB的多執行緒處理的實現。而FMDB最新的版本中主要是通過使用FMDatabaseQueue這個類來進行多執行緒處理的。

這是一個我的iOS交流群:624212887,群檔案自行下載,不管你是小白還是大牛熱烈歡迎進群 ,分享面試經驗,討論技術, 大家一起交流學習成長!希望幫助開發者少走彎路。——點選:加入

二、FMDatabaseQueue原始碼分析

我們先來看看FMDatabaseQueue如何使用。

/**
 *  FMDatabaseQueue使用案例
 */
- (void)FMDatabaseQueueTest{
    //1、獲取資料庫檔案路徑
    NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *fileName = [doc stringByAppendingPathComponent:@"students.sqlite"];

    //使用
    FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:fileName];
    [queue inDatabase:^(FMDatabase *db) {
        [db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_student_2 (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, age integer NOT NULL);"];
        [db executeUpdate:@"INSERT INTO t_student_2 (name, age) VALUES ('yixiangZZ', 20);"];
        [db executeUpdate:@"INSERT INTO t_student_2 (name, age) VALUES ('yixiangXX', 25);"];

        FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_student_2"];

        NSLog(@"%@",[NSThread currentThread]);
        while ([rs next]) {
            int ID = [rs intForColumn:@"id"];
            NSString *name = [rs stringForColumn:@"name"];
            int age = [rs intForColumn:@"age"];
            NSLog(@"%d %@ %d",ID,name,age);
        }

    }];

    //支援事務
    [queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
        [db executeUpdate:@"UPDATE t_student_2 SET age = 40 WHERE name = 'yixiangZZ'"];
        [db executeUpdate:@"UPDATE t_student_2 SET age = 45 WHERE name = 'yixiangXX'"];

        BOOL hasProblem = NO;
        if (hasProblem) {
            *rollback = YES;//回滾
            return;
        }

        FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_student_2"];
        NSLog(@"%@",[NSThread currentThread]);
        while ([rs next]) {
            int ID = [rs intForColumn:@"id"];
            NSString *name = [rs stringForColumn:@"name"];
            int age = [rs intForColumn:@"age"];
            NSLog(@"%d %@ %d",ID,name,age);
        }

    }];
}
複製程式碼

FMDB的多執行緒支援實現主要是依賴於FMDatabaseQueue這個類。下面我們來看看他是如何實現的。

2.1:初始化Queue。生成一個序列佇列。

+ (instancetype)databaseQueueWithPath:(NSString*)aPath {
    FMDatabaseQueue *q = [[self alloc] initWithPath:aPath];
    FMDBAutorelease(q);
    return q;
}
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName { 
    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生成一個標示,給_queue這個GCD佇列指定了一個kDispatchQueueSpecificKey字串,並和self(即當前FMDatabaseQueue物件)進行繫結。日後可以通過此字串獲取到繫結的物件(此處就是self)。
        dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
        _openFlags = openFlags;
    }
    return self;
}
複製程式碼

2.2:序列執行資料庫操作。

- (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);

    dispatch_sync(_queue, ^() {//序列執行block

        FMDatabase *db = [self database];
        block(db);

        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);
}
複製程式碼

之於為什麼要用dispatch_queue_set_specific和dispatch_get_specific判斷是不是當前queue,是因為為了防止多執行緒操作時候出現死鎖。可以參考告訴你告訴你dispatch_queue_set_specific和dispatch_get_specific是個什麼鬼
是個什麼鬼和被廢棄的dispatch_get_current_queue

我們可以看出,一個queue就是一個序列佇列。就算你開啟多執行緒執行,它依然還是序列執行的。保證的執行緒的安全性。看下面一個案例:

/**
 *  FMDatabaseQueue如何實現多執行緒的案例
 */
- (void)FMDatabaseQueueMutilThreadTest{
    //1、獲取資料庫檔案路徑
    NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *fileName = [doc stringByAppendingPathComponent:@"students.sqlite"];

    //使用queue1
    FMDatabaseQueue *queue1 = [FMDatabaseQueue databaseQueueWithPath:fileName];

    [queue1 inDatabase:^(FMDatabase *db) {
        for (int i=0; i<10; i++) {
            NSLog(@"queue1---%zi--%@",i,[NSThread currentThread]);
        }
    }];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [queue1 inDatabase:^(FMDatabase *db) {
            for (int i=11; i<20; i++) {
                NSLog(@"queue1---%zi--%@",i,[NSThread currentThread]);
            }
        }];
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [queue1 inDatabase:^(FMDatabase *db) {
            for (int i=20; i<30; i++) {
                NSLog(@"queue1---%zi--%@",i,[NSThread currentThread]);
            }
        }];
    });

    //雖然開啟了多個執行緒,可依然還是序列處理。原因如下:

    /**FMDatabaseQueue雖然看似一個佇列,實際上它本身並不是,它通過內部建立一個Serial的dispatch_queue_t來處理通過inDatabase和inTransaction傳入的Blocks,所以當我們在主執行緒(或者後臺)呼叫inDatabase或者inTransaction時,程式碼實際上是同步的。FMDatabaseQueue這麼設計的目的是讓我們避免發生併發訪問資料庫的問題,因為對資料庫的訪問可能是隨機的(在任何時候)、不同執行緒間(不同的網路回撥等)的請求。內建一個Serial佇列後,FMDatabaseQueue就變成執行緒安全了,所有的資料庫訪問都是同步執行,而且這比使用@synchronized或NSLock要高效得多。
     */
}
複製程式碼

執行結果如下,可以看出佇列內部就算是非同步執行,但是依然還是序列執行的:


1.png
1.png

)

雖然每個queue內部是序列執行的,當時不同的queue之間可以併發執行

案例如下:

/**
 *  FMDatabaseQueue如何實現多執行緒的案例2
 */
- (void)FMDatabaseQueueMutilThreadTest2{
    //1、獲取資料庫檔案路徑
    NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *fileName = [doc stringByAppendingPathComponent:@"students.sqlite"];

    //使用queue1
    FMDatabaseQueue *queue1 = [FMDatabaseQueue databaseQueueWithPath:fileName];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [queue1 inDatabase:^(FMDatabase *db) {
            for (int i=0; i<5; i++) {
                NSLog(@"queue1---%zi--%@",i,[NSThread currentThread]);
            }
        }];
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [queue1 inDatabase:^(FMDatabase *db) {
            for (int i=5; i<10; i++) {
                NSLog(@"queue1---%zi--%@",i,[NSThread currentThread]);
            }
        }];
    });

    //使用queue2
    FMDatabaseQueue *queue2 = [FMDatabaseQueue databaseQueueWithPath:fileName];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [queue2 inDatabase:^(FMDatabase *db) {
            for (int i=0; i<5; i++) {
                NSLog(@"queue2---%zi--%@",i,[NSThread currentThread]);
            }
        }];
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [queue2 inDatabase:^(FMDatabase *db) {
            for (int i=5; i<10; i++) {
                NSLog(@"queue2---%zi--%@",i,[NSThread currentThread]);
            }
        }];
    });

    //新建多個佇列操作同一個 就不發保證執行緒安全了。不過一般 不會這麼用。
}
複製程式碼

執行結果如下,可以看出每個佇列內部是序列執行的,佇列之間的並行執行的:


2.png
2.png

所以我們可以得到如下結論。


3.png
3.png

丟擲一個問題:如果後臺在執行大量的更新,而主執行緒也需要訪問資料庫,雖然要訪問的資料量很少,但是在後臺執行完之前,還是會阻塞主執行緒。 怎麼辦?(轉載於:FMDB 在多執行緒中的使用

解決方法:對此,robertmryan給出了一些想法:

如果你是在後臺使用的inDatabase來執行更新,可以考慮換成inTransaction,後者比前者更新起來快很多,特別是在更新量比較大的時候(比如更新1000條或10000條)。
拆解你的更新資料量,如果有300條,可以分10次、每次更新30條。當然有時不能這麼做,因為你可能通過網路請求回來的資料,你希望一次性、完整地寫入到資料庫中,雖然有侷限性,不過這確實能很好地減少每個Block佔用資料庫的時間。
上面兩點可以改善問題,但是問題依然是存在的,在大多數時候,你應該把從主執行緒呼叫inDatabase和inTransaction放在非同步裡:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [self.databaseQueue inDatabase:^(FMDatabase *db) {
        //do something...
    }];
});
複製程式碼

這種方式能解決不依賴於資料庫返回的結果的情況,如果對返回結果有依賴,就需要考慮UI上的體驗了,如加一個UIActivityIndicatorView 。

2.3:事務的實現

資料庫中的事務 也是保證資料庫安全的一種手段。一段sql語句,要麼全部成功,要麼全部不成功。

- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block {
    [self beginTransaction:NO withBlock:block];
}
- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block {
    FMDBRetain(self);
    dispatch_sync(_queue, ^() { //序列執行,保證執行緒安全。

        BOOL shouldRollback = NO;

        if (useDeferred) {
            [[self database] beginDeferredTransaction];// 使用延時性事務
        }
        else {
            [[self database] beginTransaction];// 預設使用獨佔性事務
        }

        block([self database], &shouldRollback);//執行block

        if (shouldRollback) {  //根據shouldRollback判斷 是否回滾,還是提交。
            [[self database] rollback];
        }
        else {
            [[self database] commit];
        }
    });

    FMDBRelease(self);
}
複製程式碼

關於延時性事務和獨佔性事務的區別如下:

在SQLite 3.0.8或更高版本中,事務可以是延遲的,即時的或者獨佔的。“延遲的”即是說在資料庫第一次被訪問之前不獲得鎖。 這樣就會延遲事務,BEGIN語句本身不做任何事情。直到初次讀取或訪問資料庫時才獲取鎖。對資料庫的初次讀取建立一個SHARED鎖 ,初次寫入建立一個RESERVED鎖。由於鎖的獲取被延遲到第一次需要時,別的執行緒或程式可以在當前執行緒執行BEGIN語句之後建立另外的事務 寫入資料庫。若事務是即時的,則執行BEGIN命令後立即獲取RESERVED鎖,而不等資料庫被使用。在執行BEGIN IMMEDIATE之後, 你可以確保其它的執行緒或程式不能寫入資料庫或執行BEGIN IMMEDIATE或BEGIN EXCLUSIVE. 但其它程式可以讀取資料庫。 獨佔事務在所有的資料庫獲取EXCLUSIVE鎖,在執行BEGIN EXCLUSIVE之後,你可以確保在當前事務結束前沒有任何其它執行緒或程式 能夠讀寫資料庫。

2.4:存檔與回滾

- (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, ^() { 

        NSString *name = [NSString stringWithFormat:@"savePoint%ld", savePointIdx++];

        BOOL shouldRollback = NO;

        if ([[self database] startSavePointWithName:name error:&err]) {//設定一個存檔點

            block([self database], &shouldRollback);

            if (shouldRollback) {
                // We need to rollback and release this savepoint to remove it
                [[self database] rollbackToSavePointWithName:name error:&err];//回滾到存檔點
            }
            [[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
}
複製程式碼

三、FMDatabasePool

FMDatabasePool : 使用任務池的形式,對多執行緒的操作提供支援。

不過官方對這種方式並不推薦使用(ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD),優先選擇FMDatabaseQueue的方式。

平時基本也不使用,官方也不推薦使用。這裡就不多講了。

四、參考資料

FMDB原始碼

FMDB原始碼閱讀系列

五、最後說一點

這是一個我的iOS交流群:624212887,群檔案自行下載,不管你是小白還是大牛熱烈歡迎進群 ,分享面試經驗,討論技術, 大家一起交流學習成長!希望幫助開發者少走彎路。——點選:加入

如果覺得對你還有些用,就關注小編+喜歡這一篇文章。你的支援是我繼續的動力。

下篇文章預告:ObjectC Hook函式的實現與實戰

文章來源於網路,如有侵權,請聯絡小編刪除。


相關文章