iOS 開發之 FMDB 源碼分析
概念:
FMDB 是用於數據存儲的框架,它是 iOS 平臺下對 SQLite 數據庫的封裝。FMDB 是面向對象的,它以 OC 的方式封裝了 SQLite 的 C 語言 API,使用起來更加方便。
Core Data是 ORM(對象關系映射) 的一種體現,使用Core Data需要用到模型數據的轉化,雖然操作簡單,不需要直接操作數據庫,但是性能沒有直接使用SQLite高。但是SQLite使用的時候需要使用c語言中的函數,操作比較麻煩,因此需要對它進行封裝。但是如果只是簡單地封裝,很可能會忽略很多重要的細節,比如如何處理並發以及安全性更問題。
使用第三方框架FMDB,它是對libsqlite3框架的封裝
FMDB GitHub下載地址:
重要的類:
-
FMResultSet : 表示FMDatabase執行查詢之後的結果集。
-
FMDatabase : 表示一個單獨的SQLite數據庫操作實例,用來執行SQL語句, 通過它可以對數據庫進行增刪改查等等操作。
-
FMDatabaseAdditions : 擴展FMDatabase類,新增對查詢結果只返回單個值的方法進行簡化,對表、列是否存在,版本號,校驗SQL等等功能。
-
FMDatabaseQueue : 使用串行隊列 ,對多線程的操作進行了支持,用於在多線程中執行多個查詢或更新,它是線程安全的。
-
FMDatabasePool : 使用任務池的形式,對多線程的操作提供支持。(不過官方對這種方式並不推薦使用,優先選擇FMDatabaseQueue的方式:ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD)
FMDatabaseQueue 要使用單例創建,這樣多線程調用時,數據庫操作使用一個隊列,保證線程安全。
是把數據庫的操作放到一個串行隊列中,從而保證不會在同一時間對數據庫做改動。
多線程下使用FMDatabaseQueue的操作原理就可以創建一個管理類對模型數據的存取查刪進行統一管理,可以使用工具類操作,也可以創建集成NSObject的子類進行管理,需要存取的模型類繼承此子類即可。
FMDatabaseQueue如何實現多線程?
/** * 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要高效得多。 */ }
//雖然開啟了多個線程,可依然還是串行處理。原因如下:
FMDatabaseQueue雖然看似一個隊列,實際上它本身並不是,
它通過內部創建一個 Serial 的 dispatch_queue_t 來處理通過 inDatabase 和 inTransaction 傳入的 Blocks.
所以當我們在主線程(或者後臺)調用 inDatabase 或者 inTransaction 時,代碼實際上是同步的。
FMDatabaseQueue這麽設計的目的是讓我們避免發生並發訪問數據庫的問題,因為對數據庫的訪問可能是隨機的(在任何時候)、不同線程間(不同的網絡回調等)的請求。
內置一個 Serial 隊列後,FMDatabaseQueue 就變成線程安全了,所有的數據庫訪問都是同步執行,而且這比使用 @synchronized 或 NSLock要高效得多。
雖然每個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]); } }]; }); //新建多個隊列操作同一個 就不發保證線程安全了。不過一般 不會這麽用。 }
如果後臺在執行大量的更新,而主線程也需要訪問數據庫,雖然要訪問的數據量很少,但是在後臺執行完之前,還是會阻塞主線程。 怎麽辦?
解決方案:
- 如果你是在後臺使用的
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... }]; });
iOS 開發之 FMDB 源碼分析