【原】FMDB源碼閱讀(一)

来源:http://www.cnblogs.com/polobymulberry/archive/2016/02/22/5178770.html
-Advertisement-
Play Games

【原】FMDB源碼閱讀(一) 本文轉載請註明出處 —— polobymulberry-博客園 1. 前言 說實話,之前的SDWebImage和AFNetworking這兩個組件我還是使用過的,但是對於FMDB組件我是一點都沒用過。好在FMDB源碼中的main.m文件提供了大量的示例,況且網上也有很多


【原】FMDB源碼閱讀(一)

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

1. 前言


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

// 找到用戶目錄下的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 程式的資料庫連接對象。

如果 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. 參考文章



您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.啟用藍牙並使設備處於可發現狀態 1.1 在使用BluetoothAdapter類的實例進操作之前,應啟用isEnable()方法檢查設備是否啟用了藍牙適配器。 // 使用意圖提示用戶啟用藍牙,並使設備處於可發現狀態 private void startBluetooth() { Bluetoot
  • 分類:C#、Android、VS2015; 創建日期:2016-02-23 一、簡介 這一章我們主要學習Intent的基本用法,並通過例子演示如下功能: 如何啟動另一個界面; 如何獲取另一個界面的返回值; 如何利用Intent讀取圖庫中的圖片; 如何利用Intent讀取和更新通訊錄; 如何利用Int...
  • 做蘋果開發也有段很長的時間了,斷斷續續大概已經4年了【目前沒有從事這個行業】,從剛開始在北京的一家培訓公司學習iOS開發起,到找到工作,再到丟掉工作,失去信心,再到重回開發。過程複雜。今天總結一下一些常用的蘋果電腦操作和開發環境XCODE以及終端的常用命令的一些操作知識。 首先總結一下蘋果系統的操作
  • 本文目錄 UIView+WebCacheOperation UIImageView+WebCache、UIImageView+HighlightedWebCache、MKAnnotationView+WebCache UIButton+WebCache 對於視圖分類,我們最熟悉的當屬 這個分類了。通
  • 如下代碼 DrawerLayout mdrawerLayout; Button btn; ---------------------------------------------------------------------------------------------------------
  • 現象: 升級到iOS7後,UIStatusBar的出現導致現有UI界面亂掉了。 原因: 由於寫死了某些控制項的絕對位置,原先隱藏UIStatusBar的代碼沒有在iOS7中起作用 解決方法: iOS7以下版本隱藏UIStatusBar的方法: - (BOOL)application:(UIApplic
  • ListView和Adapter的使用 首先介紹一下ListView是Android開發過程中較為常見的組件之一,它將數據以列表的形式展現出來。一般而言,一個ListView由以下三個元素組成: 1、View,用於展示列表,通常是一個xml所指定的。大家都知道Android的界面基本上是由xml文件
  • 添加約束
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...