前言
本文专注讨论FMDatabaseQueue在多线程编程环境下的实战问题,旨在如何知其所以然并写出高效的应用App代码。如果你对于多线程和队列这些概念还不是很清晰,可以阅读以下博客帮助理解:
如果你只是需要FMDatabaseQueue的实战代码,可以看看iOS FMDB多线程之FMDatabaseQueue使用。
正文
在开始之前先思考以下两个问题:
- FMDatabaseQueue是如何处理多线程问题的?
- 使用了FMDatabaseQueue之后我还需要在调用子线程以避免卡UI吗?
sqlite是一种非常有效的文件类关系型数据库,iOS很早就支持了,所以我们开发iOS应用,绝大多数情况下都在用sqlite做数据的永久化存储。但是通常情况下,你的应用会庞大而复杂【产品经理总是有那么多需求,我怎么知道】,再加上现在的A13芯片这么牛X,在多核处理下的生态之下,多线程是永远逃不掉的话题。BUT,多线程直接操作同一个数据库,会将数据库损坏,sqlite现在的版本已经是线程安全了。但是我们通常并不直接使用sqlite的api,在学习了FMDB之后我们知道,为了能够让你轻松使用sqlite的线程安全机制,FMDB推出了** FMDatabaseQueue**,用法极其优雅,好不快活:
- (BOOL)insertModelWithDBQueue:(FMDatabaseQueue *)dbQueue andTableName:(NSString *)tableName andColumns:(NSString *)columnStr andValues:(NSString *)valueStr {
__block BOOL _safe_success;
[dbQueue inDatabase:^(FMDatabase *db) {
NSString *ssql = [NSString stringWithFormat:@"INSERT OR REPLACE INTO %@ (%@) values (%@)",tableName,columnStr,valueStr];
_safe_success = [db executeUpdate:ssql];
}];
return _safe_success;
}
下面回答第一个问题:
FMDatabaseQueue通过同步串行队列解决多线程同时访问同一数据库的问题,相关代码如下,在FMDB的源码里可以找到:
//FMDataBaseQueue.m :100行
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
//FMDataBaseQueue.m :190行
dispatch_sync(_queue, ^() {
接下来看第二个问题,这也是写这篇文章的原因。先来看我在代码里留下的一段注释:
略长。且看被我选中的那句话,答案显而易见,即使用了FMDataBaseQueue,也会被卡UI,因为FMDataBaseQueue的设计目的是为了保护数据库,又不是为了让用户操作更爽更流畅哦,那是你我的责任。如何解决,上面的注释里也说的挺详细了。贴一下我的代码,以供参考。
- (void)insertModelToDataCacheWith:(NSString *)tableName
andColumns:(NSString *)columnStr
andValues:(NSString *)valueStr {
[self insertModelFastWithTableName:tableName andColumns:columnStr andValues:valueStr];
}
- (void)insertModelFastWithTableName:(NSString *)tableName
andColumns:(NSString *)columnStr
andValues:(NSString *)valueStr {
NSString *ssql = [NSString stringWithFormat:@"INSERT OR REPLACE INTO %@ (%@) values (%@)",tableName,columnStr,valueStr];
[self addLog:ssql];
}
- (void)addLog:(NSString *)logStr {
if (nil == logStr) {
return;
}
dispatch_async([self databaseSyncQueue], ^{
/**! 插入数据到0的问题,避免数组长度变化导致的越界*/
[self.valueStrs insertObject:logStr atIndex:0];
});
}
- (void)writeLogWithAnscWay {
int logCount = (int)self.valueStrs.count;
if (logCount == 0) {
return;
}
if (!self.vsLock) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.vsLock = [[NSLock alloc] init];
});
}
[self.vsLock lock];
NSArray *writeLog = [[NSArray alloc] initWithArray:self.valueStrs];
[self.valueStrs removeAllObjects];
[self.vsLock unlock];
[self.dataDBQueue inTransaction:^(FMDatabase * _Nonnull db, BOOL * _Nonnull rollback) {
for (NSString *ssql in writeLog) {
NSLog(@"%p",writeLog);
BOOL insertData = [db executeUpdate:ssql];
if (!insertData) {
NSLog(@"insertData------------------ :%@ : %@",@"FAILURE",ssql);
}
}
}];
}
- (void)checkInsertInTransaction {
dispatch_async([self databaseSyncQueue], ^{
[self writeLogWithAnscWay];
});
}
- (dispatch_queue_t)databaseSyncQueue {
static dispatch_queue_t _databaseQueue = nil;
if (_databaseQueue == nil) {
_databaseQueue = dispatch_queue_create("database-Sync-queue", 0);}
return _databaseQueue;
}
探索
问题到这里看似解决了,但是还是不免有疑问。
多线程里调用FMDataBaseQueue,那实际的数据库操作是在多个线程里完成的还是在单一线程完成的呢?
先说答案,多个线程。在上面那篇深入理解Thread线程和Queue队列有一张表格特别好,sync(create_queue)【同步串行队列】不会创建新的线程。所以你的操作从哪个线程来的就在哪个线程执行,而且任务执行方式是串行的【FIFO,先进先出】,后加入的任务会在排队。调用方式是同步的,所以调用的地方会hold住,只为等待数据库的执行结果。
实际项目中多线程访问FMDataBaseQueue的操作任务会在多个线程中以FIFO的顺序调用。下面我用一段代码示例证明上面的话,你可以拿到你的demo上试下。
- (void)testGcd {
for (int i = 0; i < 10; i ++) {
//增加点时间开销,保证多线程添加到同步队列的原始顺序。
[NSThread sleepForTimeInterval:0.1];
//开启多线程任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//多线程访问同一资源时,通过同步串行队列保证先进先出。
//FMDataBaseQueue用的就是这个,起保护数据库的目的
dispatch_sync([self bleQueue], ^{
[NSThread sleepForTimeInterval:0.2];
NSLog(@"Thread : %@ /// %d", [NSThread currentThread],i);
});
});
}
}
执行结果如图: