iOS開發原始碼閱讀篇--FMDB原始碼分析2(FMDatabase+FMDatabaseAdditions)
一、前言
如上一章所講,FMDB原始碼主要有以下幾個檔案組成:
FMResultSet : 表示FMDatabase執行查詢之後的結果集。
FMDatabase : 表示一個單獨的SQLite資料庫操作例項,通過它可以對資料庫進行增刪改查等等操作。
FMDatabaseAdditions : 擴充套件FMDatabase類,新增對查詢結果只返回單個值的方法進行簡化,對錶、列是否存在,版本號,校驗SQL等等功能。
FMDatabaseQueue : 使用序列佇列 ,對多執行緒的操作進行了支援。
FMDatabasePool : 使用任務池的形式,對多執行緒的操作提供支援。(不過官方對這種方式並不推薦使用,優先選擇FMDatabaseQueue的方式:ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD)
這一篇我們就來講講FMDatabase和FMDatabaseAdditions的實現思路。
這是一個我的iOS交流群:624212887,群檔案自行下載,不管你是小白還是大牛熱烈歡迎進群 ,分享面試經驗,討論技術, 大家一起交流學習成長!希望幫助開發者少走彎路。——點選:ofollow,noindex">加入
二、FMDatabase原始碼分析
2.1:開啟資料庫連線
-(BOOL)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; } //當執行這段程式碼的時候,資料庫正在被其他執行緒訪問,那我們就需要給他設定一個重試時間,預設為2秒。 if (_maxBusyRetryTimeInterval > 0.0) { // set the handler [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval]; } return YES; } - (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeout { _maxBusyRetryTimeInterval = timeout; if (!_db) { return; } if (timeout > 0) { sqlite3_busy_handler(_db, &FMDBDatabaseBusyHandler, (__bridge void *)(self)); } else { // turn it off otherwise sqlite3_busy_handler(_db, nil, nil); } }
setMaxBusyRetryTimeInterval設定重試時間,其實呼叫的是
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會不斷呼叫該回調函式,直到其他執行緒或者程序釋放鎖。獲得鎖之後,不會再呼叫該回調函式,從而繼續向下執行下去,進行資料庫操作。該函式是在獲取不到鎖的時候,以執行回撥函式的次數來進行延時,等待其他程序或者執行緒操作資料庫結束,從而獲得鎖進行操作資料庫。
2.2:查詢資料庫
executeQuery系列函式從根本上看其實呼叫的都是
-(FMResultSet )executeQuery:(NSString )sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args。
引數sql : 需要查詢的sql語句。
引數arrayArgs : 陣列型別的引數。應于于
FMResultSet *resultSet = [_db executeQuery:@"SELECT * FROM t_student WHERE age > ?" withArgumentsInArray:@[@25]];
引數dictionaryArgs:字典型別的引數。應用於
FMResultSet *resultSet = [_db executeQuery:@"SELECT * FROM t_student WHERE age > :age" withParameterDictionary:@{@"age":@25}];
引數args:可變引數型別。應用於
FMResultSet *resultSet = [_db executeQuery:@"SELECT * FROM t_student WHERE age > ?",@(20)];
下面來看一下該函式的大致實現過程,主要就是對sqlite3_prepare_v2的封裝。
- (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; FMStatement *statement= 0x00; FMResultSet *rs= 0x00; if (_traceExecution && sql) {//列印sql語句 NSLog(@"%@ executeQuery: %@", self, sql); } if (_shouldCacheStatements) {//獲取快取資料 statement = [self cachedStatementForQuery:sql]; pStmt = statement ? [statement statement] : 0x00; [statement reset]; } if (!pStmt) {//沒有快取資料,直接查詢資料庫 rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);//對sql語句進行預處理,生成預處理過的“sql語句”pStmt。 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(); } sqlite3_finalize(pStmt); _isExecutingStatement = NO; return nil; } } id obj; int idx = 0; int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)//queryCount獲取引數個數,“?”或者“:age”都算。 // If dictionaryArgs is passed in, that means we are using sqlite's named parameter support if (dictionaryArgs) {//對dictionaryArgs引數的處理,類似於":age"引數形式 for (NSString *dictionaryKey in [dictionaryArgs allKeys]) { // Prefix the key with a colon. NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey]; if (_traceExecution) { NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]); } // Get the index for the parameter name. int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]); FMDBRelease(parameterName); if (namedIdx > 0) { // Standard binding from here. // *將萬用字元?:age按照索引 賦值為obj。 //*SELECT * FROM t_student WHERE age > :age --> SELECT * FROM t_student WHERE age > obj [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt]; // increment the binding count, so our check below works out idx++; } else { NSLog(@"Could not find index for %@", dictionaryKey); } } } else {//對於arrayArgs引數和不定引數的處理,類似於"?"引數形式 while (idx < queryCount) { if (arrayArgs && idx < (int)[arrayArgs count]) {//陣列形式 obj = [arrayArgs objectAtIndex:(NSUInteger)idx]; } else if (args) {//不定引數形式 obj = va_arg(args, id); } else { //We ran out of arguments break; } if (_traceExecution) { if ([obj isKindOfClass:[NSData class]]) { NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]); } else { NSLog(@"obj: %@", obj); } } idx++; // *將萬用字元?:age按照索引 賦值為obj。 //*SELECT * FROM t_student WHERE age > ? --> SELECT * FROM t_student WHERE age > obj [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 if (!statement) {//生成FMStatement物件 statement = [[FMStatement alloc] init]; [statement setStatement:pStmt]; if (_shouldCacheStatements && sql) {//快取處理,key為sql語句,值為statement [self setCachedStatement:statement forQuery:sql]; } } // the statement gets closed in rs's dealloc or [rs close]; rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];//生成FMResultSet結果集物件 [rs setQuery:sql]; NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs]; [_openResultSets addObject:openResultSet]; [statement setUseCount:[statement useCount] + 1]; FMDBRelease(statement); _isExecutingStatement = NO; return rs; }
上面有一個關鍵的函式就是- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt進行引數繫結,把萬用字元形式的引數改成obj實際引數。
將萬用字元?:age按照索引 賦值為obj。比如:SELECT * FROM t_student WHERE age > :age –> SELECT * FROM t_student WHERE age > obj 或者 SELECT * FROM t_student WHERE age > ? –> SELECT * FROM t_student WHERE age > obj
2.3 更新資料庫操作
這裡的更新,並不只是單單的更新資料。而是對資料庫有更改的操作,增刪改都算。FMDB呼叫的都是executeUpdate系列函式。這個函式基本上跟executeQuery系列函式的實現基本差不多。只是它生成statement物件後,直接呼叫rc = sqlite3_step(pStmt);更新執行,而沒有像executeQuery延遲到FMResultSet中的next函式中執行。
2.4 WithFormat形式的資料查詢和更新
比如:FMResultSet resultSet = [_db executeQueryWithFormat:@”SELECT FROM t_student WHERE age > %d”,20];替換原來?萬用字元。
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... { va_list args; va_start(args, format); NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]]; NSMutableArray *arguments = [NSMutableArray array]; [self extractSQL:format argumentsList:args intoString:sql arguments:arguments]; va_end(args); return [self executeQuery:sql withArgumentsInArray:arguments]; }
從程式碼中可以看出,這裡其實是使用
/** *將format的sql語句 改為 可用的sql語句 *SELECT * FROM t_student WHERE age > %d --> SELECT * FROM t_student WHERE age > ? * *@param sqlSELECT * FROM t_student WHERE age > %d *@param args可變引數,c語言的格式化引數 *@param cleanedSQL SELECT * FROM t_student WHERE age > ? *@param argumentsoc陣列 */ - (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments;
2.5 一次性執行多條sql語句。
使用executeStatements函式可以一次性執行多條sql語句,例項如下:
/** *將多個SQL執行語句寫入一個字串中,並執行 */ - (void)executeStatementsTest{ NSString *sql = @"CREATE TABLE IF NOT EXISTS t_student_tmp (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, age integer NOT NULL);" "INSERT INTO t_student_tmp (name, age) VALUES ('yixiang', 10);" "INSERT INTO t_student_tmp (name, age) VALUES ('yixiangXX', 20);"; BOOL success = [_db executeStatements:sql]; if (success) { NSLog(@"執行成功"); }else{ NSLog(@"執行失敗"); } sql = @"SELECT * FROM t_student;" "SELECT * FROM t_student_tmp;"; success = [_db executeStatements:sql withResultBlock:^int(NSDictionary *resultsDictionary) { NSLog(@"%@",resultsDictionary);//查詢結果都在resultsDictionary return 0; }]; if (success) { NSLog(@"查詢成功"); }else{ NSLog(@"查詢失敗"); } }
它又是如何實現的呢?他其實就是對sqlite3_exec函式的封裝。
- (BOOL)executeStatements:(NSString *)sql withResultBlock:(FMDBExecuteStatementsCallbackBlock)block { int rc; char *errmsg = nil; rc = sqlite3_exec([self sqliteHandle], [sql UTF8String], block ? FMDBExecuteBulkSQLCallback : nil, (__bridge void *)(block), &errmsg); if (errmsg && [self logsErrors]) { NSLog(@"Error inserting batch: %s", errmsg); sqlite3_free(errmsg); } return (rc == SQLITE_OK); }
2.6:FMDB的加解密
FMDB中使用- [FMDatabase setKey:]和- [FMDatabase setKeyWithData:]輸入資料庫密碼以求驗證使用者身份,使用- [FMDatabase rekey:]和- [FMDatabase rekeyWithData:]來給資料庫設定密碼或者清除密碼。這兩類函式分別對sqlite3_key和sqlite3_rekey函式進行了封裝。
三、FMDatabaseAdditions原始碼分析
擴充套件FMDatabase類,新增對查詢結果只返回單個值的方法進行簡化,對錶、列是否存在,版本號,校驗SQL等等功能。
3.1:XXXForQuery系列函式
對查詢結果只有一個值得情況進行優化,有多個值也只取第一個值。用法例項:
/** *使用FMDatabaseAdditions中的intForQuery函式查詢資料,如果返回結果有多個數據只取第一條資料 */ - (void)queryForIntForQuery{ int idx = [_db intForQuery:@"SELECT id FROM t_student WHERE age = ?",@(26)]; NSLog(@"%zi",idx); }
3.2: 資料庫的一些概要資訊
-(BOOL)tableExists:(NSString*)tableName;資料庫表是否存在。
-(BOOL)columnExists:(NSString)columnName inTableWithName:(NSString )tableName;在tableName表中columnName是否存在。
-(FMResultSet*)getSchema;資料庫的一些概要資訊。例項如下:
/** *schema概要資訊 */ - (void)querySchema{ //查詢資料庫schema(所有表的一些資訊) //*對於表來說, tbl_name 則仍然是表名。 但sqlite_master 中不僅是表記錄,還有其它的物件,比如索引,對於索引來說 name 是索引的名字,而tbl_name 是索引所屬的表名。 FMResultSet *resultSet = [_db getSchema]; while ([resultSet next]) { NSString *type = [resultSet stringForColumn:@"type"]; NSString *name = [resultSet stringForColumn:@"name"]; NSString *tbl_name = [resultSet stringForColumn:@"tbl_name"]; introotpage = [resultSet intForColumn:@"rootpage"]; NSString *sql = [resultSet stringForColumn:@"sql"]; NSLog(@"\n %@ \n %@ \n %@ \n %zi \n %@",type,name,tbl_name,rootpage,sql); } /** *每一張表具體的概要資訊(也就是每一列的資訊) *get table schema: result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER] */ resultSet = [_db getTableSchema:@"t_student"]; while ([resultSet next]) { int cid = [resultSet intForColumn:@"cid"]; NSString *name = [resultSet stringForColumn:@"name"]; NSString *type = [resultSet stringForColumn:@"type"]; int notnull = [resultSet intForColumn:@"notnull"]; int pk = [resultSet intForColumn:@"pk"]; NSLog(@"\n %zi \n %@ \n %@ \n %zi \n %zi",cid,name,type,notnull,pk); } }
3.3:校驗sql語句是否合法
-(BOOL)validateSQL:(NSString)sql error:(NSError *)error;
四、聯絡方式
這是一個我的iOS交流群:624212887,群檔案自行下載,不管你是小白還是大牛熱烈歡迎進群 ,分享面試經驗,討論技術, 大家一起交流學習成長!希望幫助開發者少走彎路。——點選:加入
如果覺得對你還有些用,就關注小編+喜歡這一篇文章。你的支援是我繼續的動力。
下篇文章預告:·FMDB原始碼分析3(FMDatabase+FMDatabaseAdditions)
文章來源於網路,如有侵權,請聯絡小編刪除。