如上一章所講,FMDB源碼主要有如下幾個文件組成:html
FMResultSet : 表示FMDatabase執行查詢以後的結果集。git
FMDatabase : 表示一個單獨的SQLite數據庫操做實例,經過它能夠對數據庫進行增刪改查等等操做。github
FMDatabaseAdditions : 擴展FMDatabase類,新增對查詢結果只返回單個值的方法進行簡化,對錶、列是否存在,版本號,校驗SQL等等功能。面試
FMDatabaseQueue : 使用串行隊列 ,對多線程的操做進行了支持。sql
FMDatabasePool : 使用任務池的形式,對多線程的操做提供支持。(不過官方對這種方式並不推薦使用,優先選擇FMDatabaseQueue的方式:ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD)數據庫
FMDB比較優秀的地方就在於對多線程的處理。因此這一篇主要是研究FMDB的多線程處理的實現。而FMDB最新的版本中主要是經過使用FMDatabaseQueue這個類來進行多線程處理的。安全
這是一個個人iOS交流羣:624212887,羣文件自行下載,無論你是小白仍是大牛熱烈歡迎進羣 ,分享面試經驗,討論技術, 你們一塊兒交流學習成長!但願幫助開發者少走彎路。——點擊:加入bash
咱們先來看看FMDatabaseQueue如何使用。網絡
/**
* FMDatabaseQueue使用案例
*/
- (void)FMDatabaseQueueTest{
//一、獲取數據庫文件路徑
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這個類。下面咱們來看看他是如何實現的。多線程
+ (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;
}
複製代碼
- (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{
//一、獲取數據庫文件路徑
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要高效得多。
*/
}
複製代碼
執行結果以下,能夠看出隊列內部就算是異步執行,可是依然仍是串行執行的:
)
雖然每一個queue內部是串行執行的,當時不一樣的queue之間能夠併發執行
案例以下:
/**
* FMDatabaseQueue如何實現多線程的案例2
*/
- (void)FMDatabaseQueueMutilThreadTest2{
//一、獲取數據庫文件路徑
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]);
}
}];
});
//新建多個隊列操做同一個 就不發保證線程安全了。不過通常 不會這麼用。
}
複製代碼
執行結果以下,能夠看出每一個隊列內部是串行執行的,隊列之間的並行執行的:
因此咱們能夠獲得以下結論。
拋出一個問題:若是後臺在執行大量的更新,而主線程也須要訪問數據庫,雖然要訪問的數據量不多,可是在後臺執行完以前,仍是會阻塞主線程。 怎麼辦?(轉載於: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 。
數據庫中的事務 也是保證數據庫安全的一種手段。一段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以後,你能夠確保在當前事務結束前沒有任何其它線程或進程 可以讀寫數據庫。
- (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 : 使用任務池的形式,對多線程的操做提供支持。
不過官方對這種方式並不推薦使用(ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD),優先選擇FMDatabaseQueue的方式。
平時基本也不使用,官方也不推薦使用。這裏就很少講了。
這是一個個人iOS交流羣:624212887,羣文件自行下載,無論你是小白仍是大牛熱烈歡迎進羣 ,分享面試經驗,討論技術, 你們一塊兒交流學習成長!但願幫助開發者少走彎路。——點擊:加入
若是以爲對你還有些用,就關注小編+喜歡這一篇文章。你的支持是我繼續的動力。
下篇文章預告:ObjectC Hook函數的實現與實戰
文章來源於網絡,若有侵權,請聯繫小編刪除。