標籤:fmdb database is locked fmdatabasequeue 單例
今天終於解決了多線程同時訪問資料庫時,報資料庫鎖定的問題,錯誤資訊是:
Unknown error finalizing or resetting statement (5: database is locked)
最後通過FMDatabaseQueue解決了這個問題,本文總結一下:
FMDatabase不能多線程使用同一個執行個體
多線程訪問資料庫,不能使用同一個FMDatabase的執行個體,否則會發生異常。如果線程使用單獨的FMDatabase執行個體是允許的,但是同樣有可能發生database is locked的問題。這是由於多線程對sqlite的競爭引起的
我的app一開始就是多線程使用單獨的FMDatabase執行個體訪問資料庫,雖然沒有引起crash,但是還是出現了database is locked問題,造成很多資料沒有如預期寫入資料庫
使用FMDatabaseQueue,問題依舊
後來上FMDB的官網看了文檔,確認用FMDatabaseQueue可以解決這個問題,API也比較簡單:
NSString *dbFilePath = [PathResolver databaseFilePath];queue = [FMDatabaseQueue databaseQueueWithPath:dbFilePath];[queue inDatabase:^(FMDatabase *db){ // access db}];但是實際測試了一下,還是database is locked
讀了一下相關的源碼,FMDatabaseQueue解決這個問題的思路是:建立一個隊列,然後將放入隊列的block順序執行,這樣避免了多線程同時訪問資料庫
而My Code是多線程各建立FMDatabaseQueue的執行個體,所以其實有多個隊列,因此還是存在資料庫競爭的問題,和用FMDatabase時是一樣的
共用同一個FMDatabaseQueue執行個體
於是接下來我讓每個線程使用同一個Queue執行個體,問題就順利解決了
實現的方式,一開始我想給FMDatabase增加一個單例方法,但是這樣以後升級FMDB會比較麻煩,所以最後我是建立了一個Helper類
@implementation LosDatabaseHelper{ FMDatabaseQueue* queue;}-(id) init{ self = [super init]; if(self){ NSString *dbFilePath = [PathResolver databaseFilePath]; queue = [FMDatabaseQueue databaseQueueWithPath:dbFilePath]; } return self;}+(LosDatabaseHelper*) sharedInstance{ static dispatch_once_t pred = 0; __strong static id _sharedObject = nil; dispatch_once(&pred, ^{ _sharedObject = [[self alloc] init]; }); return _sharedObject;}-(void) inDatabase:(void(^)(FMDatabase*))block{ [queue inDatabase:^(FMDatabase *db){ block(db); }];}@end
系統中其他的類,使用這個Helper類的單例,這樣保證了全域只有唯一的FMDatabaseQueue執行個體。注意,因為Helper內部持有的是FMDatabaseQueue,所以可以這麼做,如果封裝的是FMDatabase類,就絕對會有問題。因為FMDatabase執行個體不能在多線程環境共用
使用FMDatabaseQueue之後,管理db
原本使用FMDatabase類,需要手工調用db的open和close方法
但是用FMDatabaseQueue,不需要調用open,因為查看代碼發現,Queue已經open了。至於要不要close,我也不確定,因為官方的sample code沒有調用close。實際應用中,我也沒有調用,好像沒有問題。如果需要close的話,我想可以在Helper類的公用方法裡增加調用close queue就可以了。下面是close的源碼:
- (void)close { FMDBRetain(self); dispatch_sync(_queue, ^() { [_db close]; FMDBRelease(_db); _db = 0x00; }); FMDBRelease(self);}
所以,使用Queue,是不需要自己開啟和關閉db的。但是如果使用了FMResultSet,rs倒是需要關閉,否則會報warning:
if ([db hasOpenResultSets]) { NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]");
為了不看到warning,我都在block裡調用了[rs close]
重新整理資料庫檔案路徑
具體到我們的應用,還有一個特殊問題需要考慮。因為我們的APP可以切換賬戶,而賬戶的db檔案是獨立的。所以當使用者重新登入的時候,需要重新整理一下Helper的queue
+(void) refreshDatabaseFile{ LosDatabaseHelper *instance = [self sharedInstance]; [instance doRefresh];}-(void) doRefresh{ NSString *dbFilePath = [PathResolver databaseFilePath]; queue = [FMDatabaseQueue databaseQueueWithPath:dbFilePath];}
如果不這麼做,由於Helper是單例,那麼切換賬戶以後,使用者B訪問的還是使用者A的資料庫。重新整理的調用,一般放在登入之後,進入首頁面之前就可以了
隊列和線程
在debug過程中,順便看到一個現象。雖然多個block都是放到同一個隊列裡,但是其實是跑在不同的thread裡
不要混淆隊列和線程的概念,使用GCD時,開發人員關注的是把block放到隊列中,但是同一個隊列其實可以對應多個thread,為block分配thread,是GCD架構負責的,開發人員不需要關注。只要把操作放到合適的隊列裡,GCD就會完成線程的建立,分配與回收