【原】FMDB源碼閱讀(一)

【原】FMDB源碼閱讀(一)

本文轉載請註明出處 —— polobymulberry-博客園html

1. 前言


說實話,以前的SDWebImageAFNetworking這兩個組件我仍是使用過的,可是對於FMDB組件我是一點都沒用過。好在FMDB源碼中的main.m文件提供了大量的示例,何況網上也有不少最佳實踐的例子,我就不在這獻醜了。咱們先從一個最簡單的FMDB的例子開始:sql

// 找到用戶目錄下的Documents文件夾位置
NSString* docsdir = [NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
// 將user.sqlite放到Documents文件夾下,並生成user.sqlite的絕對路徑
NSString* dbpath = [docsdir stringByAppendingPathComponent:@"user.sqlite"];
// 根據user.sqlite的絕對路徑獲取到一個FMDatabase對象,其實就是一個封裝了的SQLite數據庫對象
FMDatabase* db = [FMDatabase databaseWithPath:dbpath];
// 打開該數據庫
[db open];
// 執行SQL語句 - select * from people
FMResultSet *rs = [db 
executeQuery
:@"select * from people"];
// 利用next函數,循環輸出結果
while ([rs next]) {
    NSLog(@"%@ %@",
        [rs stringForColumn:@"firstname"], 
        [rs stringForColumn:@"lastname"]);
}
// 關閉該數據庫
[db close];

很簡單是吧,甚至我以爲上面我寫的註釋都多餘了。確實,FMDB說白了就是對SQLite數據庫的C/C++接口進行了一層封裝,固然功能也更爲強大,好比多線程操做,另外FMDB接口要比原生的SQLite接口簡潔不少。下面咱們就上面的例子研究下FMDB的基本流程。數據庫

2. FMDB的最基本流程(結合上面例子)


咱們先看看上面代碼中我用藍色粗體高亮的部分,研究下其具體實現。編程

2.1 + [FMDatabase databaseWithPath:]

// 核心其實仍是調用了+[FMDataBase initWithPath:]函數,下面會詳解
+ (instancetype)databaseWithPath:(NSString*)aPath {
    // FMDBReturnAutoReleased是爲了讓FMDB兼容MRC和ARC,具體細節看下其宏定義就明白了
    return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath]);
}

/** 初始化一個FMDataBase對象
 根據path(aPath)來建立一個SQLite數據庫。對應的aPath參數有三種情形:
 
 1. 數據庫文件路徑:不爲空字符串,不爲nil。若是該文件路徑不存在,那麼SQLite會給你新建一個
 2. 空字符串@"":將在外存臨時給你建立一個空的數據庫,而且若是該數據庫鏈接釋放,那麼對應數據庫會自動刪除
 3. nil:會在內存中建立數據庫,隨着該數據庫鏈接的釋放,也會釋放該數據庫。
 */
- (instancetype)initWithPath:(NSString*)aPath {
    // SQLite支持三種線程模式,sqlite3_threadsafe()函數的返回值能夠肯定編譯時指定的線程模式。
 // 三種模式分別爲1.單線程模式 2.多線程模式 3.串行模式 其中對於單線程模式,sqlite3_threadsafe()返回false
 // 對於另外兩個模式,則返回true。這是由於單線程模式下沒有進行互斥(mutex),因此多線程下是不安全的
    assert(sqlite3_threadsafe());     
    self = [super init];
   // 不少屬性後面再提。不過這裏值得注意的是_db竟然賦值爲nil,也就是說真正構建_db不是在initWithPath:這個函數中,這裏透露下,其實做者是將構建部分代碼放到了open函數中if (self) {
        _databasePath               = [aPath copy];
        _openResultSets             = [[NSMutableSet alloc] init];
        _db                         = nil;
        _logsErrors                 = YES;
        _crashOnErrors              = NO;
        _maxBusyRetryTimeInterval   = 2;
    }
    
    return self;
}

2.2 - [FMDatabase open]

上面提到過+ [FMDatabase databaseWithPath:]和- [FMDatabase initWithPath:]本質上只是給了數據庫一個名字,並無真實建立或者獲取數據庫。這裏的open函數纔是真正獲取到數據庫,其本質上也就是調用SQLite的C/C++接口 – sqlite3_open()緩存

sqlite3_open(const char *filename, sqlite3 **ppDb)安全

該例程打開一個指向 SQLite 數據庫文件的鏈接,返回一個用於其餘 SQLite 程序的數據庫鏈接對象。session

若是 filename 參數是 NULL 或 ':memory:',那麼 sqlite3_open() 將會在 RAM 中建立一個內存數據庫,這隻會在 session 的有效時間內持續。數據結構

若是文件名 filename 不爲 NULL,那麼 sqlite3_open() 將使用這個參數值嘗試打開數據庫文件。若是該名稱的文件不存在,sqlite3_open() 將建立一個新的命名爲該名稱的數據庫文件並打開。多線程

- (BOOL)open {
    if (_db) {
        return YES;
    }
    
    int err = sqlite3_open([self sqlitePath], (sqlite3**)&_db );
    if(err != SQLITE_OK) {
        NSLog(@"error opening!: %d", err);
        return NO;
    }
    // 若_maxBusyRetryTimeInterval大於0,那麼就調用setMaxBusyRetryTimeInterval:函數
    // setMaxBusyRetryTimeInterval:函數主要是調用sqlite3_busy_handler來處理其餘線程已經在操做數據庫的狀況,默認_maxBusyRetryTimeInterval爲2。
    // 具體該參數有什麼用,下面在FMDBDatabaseBusyHandler函數中會詳解。
    if (_maxBusyRetryTimeInterval > 0.0) {
        [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
    }
    
    return YES;
}

- (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeout {
    
    _maxBusyRetryTimeInterval = timeout;
    
    if (!_db) {
        return;
    }
    // 處理的handler設置爲FMDBDatabaseBusyHandler這個函數
    if (timeout > 0) {
        sqlite3_busy_handler(_db, &FMDBDatabaseBusyHandler, (__bridge void *)(self));
    }
    else {
        // 不使用任何busy handler處理
        sqlite3_busy_handler(_db, nil, nil);
    }
}

這裏須要提一下sqlite3_busy_handler這個函數:併發

int sqlite3_busy_handler(sqlite3*, int(*)(void*,int), void*);

第一個參數是告知哪一個數據庫須要設置busy handler。

第二個參數是其實就是回調函數(busy handler)了,當你調用該回調函數時,需傳遞給它的一個void*的參數的拷貝,也即sqlite3_busy_handler的第三個參數;另外一個須要傳給回調函數的int參數是表示此次鎖事件,該回調函數被調用的次數。若是回調函數返回0時,將再也不嘗試再次訪問數據庫而返回SQLITE_BUSY或者SQLITE_IOERR_BLOCKED。若是回調函數返回非0, 將會不斷嘗試操做數據庫。

總結:程序運行過程當中,若是有其餘進程或者線程在讀寫數據庫,那麼sqlite3_busy_handler會不斷調用回調函數,直到其餘進程或者線程釋放鎖。得到鎖以後,不會再調用回調函數,從而向下執行,進行數據庫操做。該函數是在獲取不到鎖的時候,以執行回調函數的次數來進行延遲,等待其餘進程或者線程操做數據庫結束,從而得到鎖操做數據庫。

你們也看出來了,sqlite3_busy_handler函數的關鍵就是這個回調函數了,此處做者定義的是一個名叫FMDBDatabaseBusyHandler的函數做爲其busy handler。

// 注意:appledoc(生成文檔的軟件)中,對於有具體實現的C函數,好比下面這個函數,
// 是有bug的。因此你在生成文檔時,忽略.m文件。

// 該函數就是簡單調用sqlite3_sleep來掛起進程
static int FMDBDatabaseBusyHandler(void *f, int count) {
    FMDatabase *self = (__bridge FMDatabase*)f;
    // 若是count爲0,表示的第一次執行回調函數
    // 初始化self->_startBusyRetryTime,供後面計算delta使用
    if (count == 0) {
        self->_startBusyRetryTime = [NSDate timeIntervalSinceReferenceDate];
        return 1;
    }
    // 使用delta變量控制執行回調函數的次數,每次掛起50~100ms
    // 因此maxBusyRetryTimeInterval的做用就在這體現出來了
    // 當掛起的時長大於maxBusyRetryTimeInterval,就返回0,並中止執行該回調函數了
    NSTimeInterval delta = [NSDate timeIntervalSinceReferenceDate] - (self->_startBusyRetryTime);
    
    if (delta < [self maxBusyRetryTimeInterval]) {
         // 使用sqlite3_sleep每次當前線程掛起50~100ms
        int requestedSleepInMillseconds = (int) arc4random_uniform(50) + 50;
        int actualSleepInMilliseconds = sqlite3_sleep(requestedSleepInMillseconds); 
        // 若是實際掛起的時長與想要掛起的時長不一致,多是由於構建SQLite時沒將HAVE_USLEEP置爲1
        if (actualSleepInMilliseconds != requestedSleepInMillseconds) {
            NSLog(@"WARNING: Requested sleep of %i milliseconds, but SQLite returned %i. Maybe SQLite wasn't built with HAVE_USLEEP=1?", requestedSleepInMillseconds, actualSleepInMilliseconds);
        }
        return 1;
    }
    
    return 0;
}

2.3 - [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:](重點)

爲何不講 - [FMDatabase executeQuery:]?由於- [FMDatabase executeQuery:]等等相似的函數,最終都是對- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]的簡單封裝。該函數比較關鍵,主要是針對查詢的sql語句。

- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
    // 判斷當前是否存在數據庫以供操做
    if (![self databaseExists]) {
        return 0x00;
    }
    // 若是當前線程已經在使用數據庫了,那就輸出正在使用的警告
    if (_isExecutingStatement) {
        [self warnInUse];
        return 0x00;
    }
    
    _isExecutingStatement = YES;
    
    int rc                  = 0x00; 
    sqlite3_stmt *pStmt     = 0x00; // sqlite的prepared語句類型
    FMStatement *statement  = 0x00; // 對sqlite3_stmt的簡單封裝,在實際應用中,你不該直接操做FMStatement對象
    FMResultSet *rs         = 0x00; // FMResultSet對象是用來獲取最終查詢結果的
    // 須要追蹤sql執行狀態的話,輸出執行狀態
    if (_traceExecution && sql) {
        NSLog(@"%@ executeQuery: %@", self, sql);
    }
    // 調用sql語句以前,首先要將sql字符串預處理一下,轉化爲SQLite可用的prepared語句(預處理語句)
    // 使用sqlite3_prepare_v2來生成sql對應的prepare語句(即pStmt)代價很大
    // 因此建議使用緩存機制來減小對sqlite3_prepare_v2的使用
    if (_shouldCacheStatements) {
        // 獲取到緩存中的prepared語句
        statement = [self cachedStatementForQuery:sql];
        pStmt = statement ? [statement statement] : 0x00;
        // prepared語句能夠被重置(調用sqlite3_reset函數),而後能夠從新綁定參數以便從新執行。
        [statement reset];
    }
    // 若是緩存中沒有sql對應的prepared語句,那麼只能使用sqlite3_prepare_v2函數進行預處理
    if (!pStmt) {
        
        rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);
        // 若是生成prepared語句出錯,那麼就根據是否須要打印錯誤信息(_logsErrors)以及是否遇到錯誤直接停止程序執行(_crashOnErrors)來執行出錯處理。
        // 最後調用sqlite3_finalize函數釋放全部的內部資源和sqlite3_stmt數據結構,有效刪除prepared語句
        if (SQLITE_OK != rc) {
            if (_logsErrors) {
                NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
                NSLog(@"DB Query: %@", sql);
                NSLog(@"DB Path: %@", _databasePath);
            }
            
            if (_crashOnErrors) {
                NSAssert(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
              // abort()函數表示停止程序執行,直接從調用的地方跳出。
                abort();
            }
            
            sqlite3_finalize(pStmt);
            _isExecutingStatement = NO;
            return nil;
        }
    }
    
    id obj;
    int idx = 0;
    // 獲取到pStmt中須要綁定的參數個數
    int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)
    
    // 舉一個使用dictionaryArgs的例子
    
/**

 NSMutableDictionary 
*dictionaryArgs = [NSMutableDictionary dictionary]; [dictionaryArgs setObject: @"Text1" forKey:@"a" ]; [db executeQuery:@"select * from namedparamcounttest where a = :a" withParameterDictionary:dictionaryArgs]; // 注意相似:AAA前面有冒號的就是參數 // 其餘的參數形式如:"?", "?NNN", ":AAA", "$AAA", 或 "@AAA" */
    if (dictionaryArgs) {
        
        for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
            
            // 在每一個dictionaryKey以前加上冒號,好比上面的a -> :a,方便獲取參數在prepared語句中的索引
            NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
            // 查看執行情況
            if (_traceExecution) {
                NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
            }
            
            // 在prepared語句中查找對應parameterName的參數索引值namedIdx
            int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
            
            FMDBRelease(parameterName);
             // 能夠利用索引namedIdx獲取對應參數,再使用bindObject:函數將dictionaryArgs保存的value綁定給對應參數
            if (namedIdx > 0) {
                [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
                // 使用這個idx來判斷sql中的全部參數值是否都綁定上了
                idx++;
            }
            else {
                NSLog(@"Could not find index for %@", dictionaryKey);
            }
        }
    }
    else {
        
        while (idx < queryCount) {
            // 使用arrayArgs的例子
            /**
             [db executeQuery:@"insert into testOneHundredTwelvePointTwo values (?, ?)" withArgumentsInArray:[NSArray arrayWithObjects:@"one", [NSNumber numberWithInteger:2], nil]];
             */
            if (arrayArgs && idx < (int)[arrayArgs count]) {
                obj = [arrayArgs objectAtIndex:(NSUInteger)idx];
            }
        // 使用args的例子,使用args其實就是調用- (FMResultSet *)executeQuery:(NSString*)sql, ...;
        /**
          FMResultSet *rs = [db executeQuery:@"select rowid,* from test where a = ?", @"hi'"];
         */
            else if (args) {
                obj = va_arg(args, id);
            }
            else {
                break;
            }
            
            if (_traceExecution) {
                if ([obj isKindOfClass:[NSData class]]) {
                    NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]);
                }
                else {
                    NSLog(@"obj: %@", obj);
                }
            }
            
            idx++;
            // 綁定參數值
            [self bindObject:obj toColumn:idx inStatement:pStmt];
        }
    }
    // 若是綁定的參數數目不對,認爲出錯,並釋放資源
    if (idx != queryCount) {
        NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)");
        sqlite3_finalize(pStmt);
        _isExecutingStatement = NO;
        return nil;
    }
    
    FMDBRetain(statement); // to balance the release below
    // statement不爲空,進行緩存
    if (!statement) {
        statement = [[FMStatement alloc] init];
        [statement setStatement:pStmt];
        // 使用sql做爲key來緩存statement(即sql對應的prepare語句)
        if (_shouldCacheStatements && sql) {
            [self setCachedStatement:statement forQuery:sql];
        }
    }
    
    // 根據statement和self(FMDatabase對象)構建一個FMResultSet對象,此函數中僅僅是構建該對象,還沒使用next等函數獲取查詢結果
    // 注意FMResultSet中含有如下成員(除了最後一個,其餘成員均在此處初始化過了)
    /**
      @interface FMResultSet : NSObject {
           FMDatabase          *_parentDB; // 表示該對象查詢的數據庫,主要是爲了能在FMResultSet本身的函數中索引到正在操做的FMDatabase對象
           FMStatement         *_statement; // prepared語句
    
           NSString            *_query; // 對應的sql查詢語句
           NSMutableDictionary *_columnNameToIndexMap; 
       }
     */
    rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
    [rs setQuery:sql];
    // 將此時的FMResultSet對象添加_openResultSets,主要是爲了調試
    NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
    [_openResultSets addObject:openResultSet];
    // 並設置statement的使用數目useCount加1,暫時不清楚此成員有何做用,感受也是用於調試
    [statement setUseCount:[statement useCount] + 1];
    
    FMDBRelease(statement);
    // 生成statement的操做已經結束
    _isExecutingStatement = NO;
    
    return rs;
}

2.4 - [FMResultSet nextWithError:]

- [FMResultSet next]函數其實就是對nextWithError:的簡單封裝。做用就是從咱們上一步open中獲取到的FMResultSet對象中讀取查詢後結果的每一行,交給用戶本身處理。讀取每一行的方法(即next)其實就是封裝了sqlite3_step函數。而nextWithError:主要封裝了對sqlite3_step函數返回結果的處理。

int sqlite3_step(sqlite3_stmt*);

sqlite3_prepare函數將SQL命令字符串解析並轉換爲一系列的命令字節碼,這些字節碼最終被傳送到SQlite3的虛擬數據庫引擎(VDBE: Virtual Database Engine)中執行,完成這項工做的是sqlite3_step函數。好比一個SELECT查詢操做,sqlite3_step函數的每次調用都會返回結果集中的其中一行,直到再沒有有效數據行了。每次調用sqlite3_step函數若是返回SQLITE_ROW,表明得到了有效數據行,能夠經過sqlite3_column函數提取某列的值。若是調用sqlite3_step函數返回SQLITE_DONE,則表明prepared語句已經執行到終點了,沒有有效數據了。不少命令第一次調用sqlite3_step函數就會返回SQLITE_DONE,由於這些SQL命令不會返回數據。對於INSERT,UPDATE,DELETE命令,會返回它們所修改的行號——一個單行單列的值。

// 返回YES表示從數據庫中獲取到了下一行數據
- (BOOL)nextWithError:(NSError **)outErr {
    // 嘗試步進到下一行
    int rc = sqlite3_step([_statement statement]);
  
    // 對返回結果rc進行處理

    /**
      SQLITE_BUSY 數據庫文件有鎖
      SQLITE_LOCKED 數據庫中的某張表有鎖
      SQLITE_DONE sqlite3_step()執行完畢
      SQLITE_ROW sqlite3_step()獲取到下一行數據
      SQLITE_ERROR 通常用於沒有特別指定錯誤碼的錯誤,就是說函數在執行過程當中發生了錯誤,但沒法知道錯誤發生的緣由。
      SQLITE_MISUSE 沒有正確使用SQLite接口,好比一條語句在sqlite3_step函數執行以後,沒有被重置以前,再次給其綁定參數,這時bind函數就會返回SQLITE_MISUSE。
      */  
    if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
        NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [_parentDB databasePath]);
        NSLog(@"Database busy");
        if (outErr) {
            // lastError使用sqlite3_errcode獲取到錯誤碼,封裝成NSError對象返回
            *outErr = [_parentDB lastError];
        }
    }
    else if (SQLITE_DONE == rc || SQLITE_ROW == rc) {
        // all is well, let's return.
    }
    else if (SQLITE_ERROR == rc) {
        // sqliteHandle就是獲取到對應FMDatabase對象,而後使用sqlite3_errmsg來獲取錯誤碼的字符串
        NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
        if (outErr) {
            *outErr = [_parentDB lastError];
        }
    }
    else if (SQLITE_MISUSE == rc) {
        // uh oh.
        NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
        if (outErr) {
            if (_parentDB) {
                *outErr = [_parentDB lastError];
            }
            else {
                // 若是next和nextWithError函數是在當前的FMResultSet關閉以後調用的
                // 這時輸出的錯誤信息應該是parentDB不存在
                NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:@"parentDB does not exist" forKey:NSLocalizedDescriptionKey];
                *outErr = [NSError errorWithDomain:@"FMDatabase" code:SQLITE_MISUSE userInfo:errorMessage];
            }
            
        }
    }
    else {
        // wtf?
        NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
        if (outErr) {
            *outErr = [_parentDB lastError];
        }
    }
    
    // 若是不是讀取下一行數據,那麼就關閉數據庫
    if (rc != SQLITE_ROW) {
        [self close];
    }
    
    return (rc == SQLITE_ROW);
}

2.5 - [FMDatabase close]

與open函數成對調用。主要仍是封裝了sqlite_close函數。

- (BOOL)close {
    // 清除緩存的prepared語句,下面會詳解
    [self clearCachedStatements];
    // 關閉全部打開的FMResultSet對象,目前看來這個_openResultSets大概也是用來調試的 
    [self closeOpenResultSets];
    
    if (!_db) {
        return YES;
    }
    
    int  rc;
    BOOL retry;
    BOOL triedFinalizingOpenStatements = NO;
    
    do {
        retry   = NO;
        // 調用sqlite3_close來嘗試關閉數據庫
        rc      = sqlite3_close(_db);
// 若是當前數據庫上鎖,那麼就先嚐試從新關閉(置retry爲YES) // 同時還嘗試釋放數據庫中的prepared語句資源
        if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
            if (!triedFinalizingOpenStatements) {
                triedFinalizingOpenStatements = YES;
                sqlite3_stmt *pStmt;
// sqlite3_next_stmt(sqlite3 *pDb, sqlite3_stmt *pStmt
)表示從數據庫pDb中對應的pStmt語句開始一個個往下找出相應prepared語句,若是pStmt爲nil,那麼就從pDb的第一個prepared語句開始。
 // 此處迭代找到數據庫中全部prepared語句,釋放其資源。
                while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) {
                    NSLog(@"Closing leaked statement");
                    sqlite3_finalize(pStmt);
                    retry = YES;
                }
            }
        }
        // 關閉出錯,輸出錯誤碼
        else if (SQLITE_OK != rc) {
            NSLog(@"error closing!: %d", rc);
        }
    }
    while (retry);
    
    _db = nil;
    return YES;
}

//  _cachedStatements是用來緩存prepared語句的,因此清空_cachedStatements就是將每一個緩存的prepared語句釋放
// 具體實現就是使用下面那個close函數,close函數中調用了sqlite_finalize函數釋放資源
- (void)clearCachedStatements {
    
    for (NSMutableSet *statements in [_cachedStatements objectEnumerator]) {
        // makeObjectsPerformSelector會併發執行同一件事,因此效率比for循環一個個執行要快不少
        [statements makeObjectsPerformSelector:@selector(close)];
    }
    
    [_cachedStatements removeAllObjects];
}
// 注意:此爲FMResultSet的close函數
- (void)close {
    if (_statement) {
        sqlite3_finalize(_statement);
        _statement = 0x00;
    }
    
    _inUse = NO;
}

// 清除_openResultSets
- (void)closeOpenResultSets {
    //Copy the set so we don't get mutation errors
    NSSet *openSetCopy = FMDBReturnAutoreleased([_openResultSets copy]);
    // 迭代關閉_openResultSets中的FMResultSet對象
    for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
        FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
        // 清除FMResultSet的操做
        [rs setParentDB:nil];
        [rs close];
        
        [_openResultSets removeObject:rsInWrappedInATastyValueMeal];
    }
}

3. 總結

本文結合一個基本的FMDB使用案例,介紹了FMDB基本的運做流程和內部實現。總的來講,FMDB就是對SQLite的封裝,因此學習FMDB根本仍是在學習SQLite數據庫操做。

4. 參考文章


相關文章
相關標籤/搜索