• 【原】FMDB源码阅读(三)


    【原】FMDB源码阅读(三)

    本文转载请注明出处 —— polobymulberry-博客园

    1. 前言


    FMDB比较优秀的地方就在于对多线程的处理。所以这一篇主要是研究FMDB的多线程处理的实现。而FMDB最新的版本中主要是通过使用FMDatabaseQueue这个类来进行多线程处理的。

    2. FMDatabaseQueue使用举例


    // 创建,最好放在一个单例的类中
    FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
    
    // 使用
    [queue 
    inDatabase
    :^(FMDatabase *db) {
        [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
        [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
        [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
    
        FMResultSet *rs = [db executeQuery:@"select * from foo"];
        while ([rs next]) {
            //
        }
    }];
    
    // 如果要支持事务
    [queue 
    inTransaction
    :^(FMDatabase *db, BOOL *rollback) {
        [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
        [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
        [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
    
        if (whoopsSomethingWrongHappened) {
            *rollback = YES;
            return;
        }
        // etc…
        [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:4]];
    }];

    我们可以看到FMDB的多线程实现主要是依赖于FMDatabaseQueue这个类。下面我们结合上面这个例子,来具体看看FMDatabaseQueue的内部实现。

    2.1 + [FMDatabaseQueue databaseQueueWithPath:]

    // 调用initWithPath:函数构建一个FMDatabaseQueue对象
    + (instancetype)databaseQueueWithPath:(NSString*)aPath {
        FMDatabaseQueue *q = [[self alloc] initWithPath:aPath];
        FMDBAutorelease(q);
        return q;
    }

    查看initWithPath:函数,发现其本质是调用 - (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName函数。

    // 使用aPath作为数据库名称,并传入openFlags和vfsName作为openWithFlags:vfs:函数的参数
    // 初始化一个database和相应的queue
    - (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName {
        // 除了另外定义了一个_queue外,其他部分和FMDatabase的初始化没什么不同
        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这个GCD队列指定了一个kDispatchQueueSpecificKey字符串,并和self(即当前FMDatabaseQueue对象)进行绑定。日后可以通过此字符串获取到绑定的对象(此处就是self)。当然,你要保证正在执行的GCD队列是你之前指定的那个_queue队列。是不是有objc_setAssociatedObject函数的感觉。
             此步骤的作用后面inDatabase函数中会具体讲解。
             */ 
            dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
            _openFlags = openFlags;
        }
        
        return self;
    }

    2.2 – [FMDatabaseQueue inDatabase:]

    注意inDatabase的参数是一个block。这个block一般是封装了数据库的操作,另外这个block在inDatabase中是同步执行的。

    - (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);
        // 在当前这个queue中同步执行block
        dispatch_sync(_queue, ^() {
            
            FMDatabase *db = [self database];
            block(db);
            // 下面这部分你也看到了,定义了DEBUG宏,明显是用来调试用的。就不赘述了
            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);
    }

    其实我们从这个函数中就可以看出FMDatabaseQueue具体是怎么完成多线程的:

    image

    2.3 – [FMDatabaseQueue inTransaction:]

    该函数主要是针对数据库事务的处理:

    - (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block {
        [self beginTransaction:NO withBlock:block];
    }

    可以看到,内部直接封装的是beginTransaction:withBlock:函数,那我们直接来看beginTransaction:withBlock:函数。

    - (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block {
        FMDBRetain(self);
        dispatch_sync(_queue, ^() { 
            
            BOOL shouldRollback = NO;
            
            if (useDeferred) {
               // 如果使用延迟事务,那么就调用该函数,下面有对该函数的详解
               // 想令useDeferred为YES,可以调用与inTransaction相对的inDeferredTransaction函数
                [[self database] beginDeferredTransaction];
            }
            else {
                // 默认使用排他事务,下面有排他事务的详解
                [[self database] beginTransaction];
            }
            // 注意该block除了要创建相应的数据库事务,还需要根据需要选择是否需要回滚
             // 比如上面如果数据库操作出错了,那么你可以设置需要回滚,即返回shouldRollback为YES
            block([self database], &shouldRollback);
            // 如果需要回滚,那么就调用FMDatabase的rollback函数
            if (shouldRollback) {
                [[self database] rollback];
            }
              // 如果不需要回滚,那么就调用FMDatabase的commit函数确认提交相应SQL操作
            else {
                [[self database] commit];
            }
        });
        
        FMDBRelease(self);
    }
    
    // 通过执行rollback transaction语句来执行回滚操作
    - (BOOL)rollback {
        BOOL b = [self executeUpdate:@"rollback transaction"];
        // 既然已经回滚了,那么表示是否在进行事务的_inTransaction属性也要置为NO
        if (b) {
            _inTransaction = NO;
        }
        
        return b;
    }
    // 通过执行commit transaction语句来执行提交事务操作
    - (BOOL)commit {
        BOOL b =  [self executeUpdate:@"commit transaction"];
        // 既然已经提交过事务了,那么表示是否在进行事务的_inTransaction属性也要置为NO
        if (b) {
            _inTransaction = NO;
        }
        
        return b;
    }
    // 延迟事务指的是在对数据库操作前不进行任何加锁。默认情况下,
    // 如果仅仅用BEGIN开始一个事务,那么事务就是DEFERRED的,同时它不会获取任何锁
    - (BOOL)beginDeferredTransaction {
        
        BOOL b = [self executeUpdate:@"begin deferred transaction"];
        if (b) {
            _inTransaction = YES;
        }
        
        return b;
    }
    
    // 默认进行的是排他(exclusive)操作
    // 排他操作的实质是在开始对数据库读写前,获得EXCLUSIVE锁,即排他锁。排它锁说白点就是
    // 告诉数据库别的连接:你们不要追她了,她是我老婆了。
    - (BOOL)beginTransaction {
        
        BOOL b = [self executeUpdate:@"begin exclusive transaction"];
        if (b) {
            _inTransaction = YES;
        }
        
        return b;
    }

    2.4 – [FMDatabaseQueue inSavePoint:]

    savepoint类似于游戏存档一样的东西,一般的rollback相当于游戏重新开始,而加了savepoint后,相当于回到存档的位置然后接着游戏。与inDatabase和inTransaction相对有一个inSavePoint:的方法(相当于加了save point功能的inDatabase函数)。

    /*
     save point功能只在SQLite3.7及以上版本中使用,所以下面多数代码加上了
         #if SQLITE_VERSION_NUMBER >= 3007000
        #else
        #endif
     */
    - (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, ^() { 
            // 设定savepoint的名称,即给游戏存档设一个名字
            NSString *name = [NSString stringWithFormat:@"savePoint%ld", savePointIdx++];
            // 默认不回滚
            BOOL shouldRollback = NO;
            // 在执行block之前,先进行存档(save point)。如果有问题,直接退回这个存档(save point)
            if ([[self database] startSavePointWithName:name error:&err]) {
                
                block([self database], &shouldRollback);
                // 如果需要回滚,调用rollbackToSavePointWithName:error:回滚到存档位置(savepoint)
                if (shouldRollback) {
                    [[self database] rollbackToSavePointWithName:name error:&err];
                }
                // 记得执行完block后,不管有没有回滚,还需要释放掉这个存档
                [[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
    }
    
    // 调用savepoint $savepointname的SQL语句对数据库操作进行存档
    - (BOOL)startSavePointWithName:(NSString*)name error:(NSError**)outErr {
    #if SQLITE_VERSION_NUMBER >= 3007000
        NSParameterAssert(name);
        
        NSString *sql = [NSString stringWithFormat:@"savepoint '%@';", FMDBEscapeSavePointName(name)];
        
        return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
    #else
        NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
        if (self.logsErrors) NSLog(@"%@", errorMessage);
        return NO;
    #endif
    }
    
    // 使用release savepoint $savepointname的SQL语句删除存档,主要是为了释放资源
    - (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError**)outErr {
    #if SQLITE_VERSION_NUMBER >= 3007000
        NSParameterAssert(name);
        
        NSString *sql = [NSString stringWithFormat:@"release savepoint '%@';", FMDBEscapeSavePointName(name)];
    
        return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
    #else
        NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
        if (self.logsErrors) NSLog(@"%@", errorMessage);
        return NO;
    #endif
    }
    
    // 调用rollback transaction to savepoint $savepointname的SQL语句来回退到存档处
    - (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError**)outErr {
    #if SQLITE_VERSION_NUMBER >= 3007000
        NSParameterAssert(name);
        
        NSString *sql = [NSString stringWithFormat:@"rollback transaction to savepoint '%@';", FMDBEscapeSavePointName(name)];
    
        return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
    #else
        NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
        if (self.logsErrors) NSLog(@"%@", errorMessage);
        return NO;
    #endif
    }

    3. FMDatabasePool(建议使用FMDatabaseQueue)


    Tip:

    除非你真的知道在什么情况下(比如所有操作均为读操作)可以使用FMDatabasePool,否则尽量改用FMDatabaseQueue,不然可能会引起死锁。

    4. 总结


    FMDB比较常用的几个类基本上学习完毕。FMDB代码上不是很难,核心还是SQLite3和数据库的知识。更重要的还是要知道真实环境中的最佳实践。

    5. 参考文献


  • 相关阅读:
    【解决】Ubuntu命令行弹出光驱的方法
    【8086汇编基础】02寻址方式和MOV指令
    【8086汇编基础】03变量、数组和常量的处理
    Ubuntu12.10 内核源码外编译 linux模块编译驱动模块的基本方法
    【8086汇编基础】05常用函数库文件emu8086.inc
    Ubuntu12.10 使用DNW传数据 进行ARM开发板烧写
    【8086汇编基础】04中断
    【8086汇编基础】01汇编语言简要介绍
    飞凌OK6410开发板移植uboot官方最新版uboot2012.10.tar.bz2
    【8086汇编基础】00基础知识各种进制的数据
  • 原文地址:https://www.cnblogs.com/polobymulberry/p/5214287.html
Copyright © 2020-2023  润新知