ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
在MediaProvider中触发数据库的是attach函数,其代码如下: **MediaProvider::attach** ~~~ private Uri attachVolume(String volume) { Contextcontext = getContext(); DatabaseHelper db; if(INTERNAL_VOLUME.equals(volume)) { ......//针对内部存储空间的数据库 } elseif (EXTERNAL_VOLUME.equals(volume)) { ...... String dbName = "external-" +Integer.toHexString(volumeID) + ".db"; //①构造一个DatabaseHelper对象 db =new DatabaseHelper(context, dbName, false, false, mObjectRemovedCallback); ......//省略不相关的内容 }...... if(!db.mInternal) { //②调用DatabaseHelper的getWritableDatabase函数,该函数返回值的类型为 //SQLiteDatabase,即代表SQLite数据库的对象 createDefaultFolders(db.getWritableDatabase()); ....... } ...... } ~~~ 以上代码中列出了两个关键点,分别是: - 构造一个DatabaseHelper对象。 - 调用DatabaseHelper对象的getWritableDatabase函数得到一个代表SQLite数据库的SQLiteDatabase对象。 1. DatabaseHelper分析 DatabaseHelper是MediaProvider的内部类,它从SQLiteOpenHelper派生,其构造函数的代码如下: (1) DatabaseHelper构造函数分析 **MediaProvider.java::DatabaseHelper** ~~~ public DatabaseHelper(Context context, Stringname, boolean internal, boolean earlyUpgrade, SQLiteDatabase.CustomFunction objectRemovedCallback) { //重点关注其基类的构造函数 super(context, name, null,DATABASE_VERSION); mContext = context; mName= name; mInternal = internal; mEarlyUpgrade = earlyUpgrade; mObjectRemovedCallback = objectRemovedCallback; } ~~~ SQLiteOpenHelper作为DatabaseHelper的基类,其构造函数的代码如下: **SQLiteOpenHelper.java::SQLiteOpenHelper** ~~~ public SQLiteOpenHelper(Context context, Stringname, CursorFactory factory, int version) { //调用另外一个构造函数,注意它新建了一个默认的错误处理对象 this(context, name, factory, version, newDefaultDatabaseErrorHandler()); } public SQLiteOpenHelper(Context context, Stringname, CursorFactory factory, int version, DatabaseErrorHandlererrorHandler) { ...... mContext= context; mName =name; //看到”factory“一词,读者要能想到设计模式中的工厂模式,在本例中该变量为null mFactory= factory; mNewVersion = version; mErrorHandler= errorHandler; } ~~~ 上面这些函数都比较简单,其中却蕴含一个较为深刻的设计理念,具体如下: 从SQLiteOpenHelper的构造函数中可知,MediaProvider对应的数据库对象(即SQLiteDatabase实例)并不在该函数中创建。那么,代表数据库的SQLiteDatabase实例是何时创建呢? 此处使用了所谓的延迟创建(lazy creation)的方法,即SQLiteDatabase实例真正创建的时机是在第一次使用它的时候,也就是本例中第二个关键点函数getWritableDatabase。 在分析getWritableDatabase函数之前,先介绍一些和延迟创建相关的知识。 延迟创建或延迟初始化(lazy intializtion)所谓的“重型”资源(如占内存较大或创建时间比较长的资源),是系统开发和设计中常用的一种策略[^①]。在使用这种策略时,开发人员不仅在资源创建时“斤斤计较”,在资源释放的问题上也是“慎之又。资源释放的控制一般会采用引用计数技术。 结合前面对SQLiteDatabase的介绍会发现,SQLiteDatabase这个框架,在设计时不是简单地将SQLite API映射到Java层,而是有大量更为细致的考虑。例如,在这个框架中,资源创建采用了lazy creation方法,资源释放又利用SQLiteClosable来控制生命周期。 * * * * * **建议**:此框架虽更完善、更具扩展性,但是使用它比直接使用SQLite API要复杂得多,因此在开发过程中,应当根据实际情况综合考虑是否使用该框架。例如,笔者在开发公司的DLNA解决方案时,就直接使用了SQLiteAPI,而没使用这个框架。 * * * * * (2) getWritableDatabase函数分析 现在来看getWritableDatabase的代码。 **SQLiteDatabase.java::getWritableDatabase** ~~~ public synchronized SQLiteDatabasegetWritableDatabase() { if(mDatabase != null) { //第一次调用该函数时mDatabase还未创建。以后的调用将直接返回已经创建好的mDatabase } booleansuccess = false; SQLiteDatabase db = null; if (mDatabase != null) mDatabase.lock(); try { mIsInitializing = true; if(mName == null) { db = SQLiteDatabase.create(null); }else { //①调用Context的openOrCreateDatabase创建数据库 db = mContext.openOrCreateDatabase(mName, 0, mFactory, mErrorHandler); } intversion = db.getVersion(); if(version != mNewVersion) { db.beginTransaction(); try { if (version == 0) { /* 如果初次创建该数据库(即对应的数据库文件不存在),则调用子类实现的 onCreate函数。子类实现的onCreate函数将完成数据库建表等操作。读者不妨 查看MediaProviderDatabaseHelper实现的onCreate函数 */ onCreate(db); } else { //如果从数据库文件中读出来的版本号与MediaProvider设置的版本号不一致, //则调用子类实现的onDowngrade或onUpgrade做相应处理 if (version > mNewVersion) onDowngrade(db, version,mNewVersion); else onUpgrade(db, version,mNewVersion); } db.setVersion(mNewVersion); db.setTransactionSuccessful(); }finally { db.endTransaction(); } }// if (version != mNewVersion)判断结束 onOpen(db);//调用子类实现的onOpen函数 success =true; return db; }...... ~~~ 由以上代码可知,代表数据库的SQLiteDatabase对象是由context openOrCreateDatabase创建的。下面单起一节具体分析此函数。 2. ContextImpl openOrCreateDatabase分析 (1) openOrCreateDatabase函数分析 相信读者已能准确定位openOrCreateDatabase函数的真正实现了,它就在ContextImpl.java中,其代码如下: **ContextImpl.java::openOrCreateDatabase** ~~~ public SQLiteDatabase openOrCreateDatabase(Stringname, int mode, CursorFactory factory,DatabaseErrorHandler errorHandler) { File f =validateFilePath(name, true); //调用SQLiteDatabase的静态函数openOrCreateDatabase创建数据库 SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f.getPath(), factory, errorHandler); setFilePermissionsFromMode(f.getPath(), mode, 0); return db; } ~~~ **SQLiteDatabase.java::openDatabase** ~~~ public static SQLiteDatabase openDatabase(Stringpath, CursorFactory factory, int flags,DatabaseErrorHandlererrorHandler) { //又调用openDatabase创建SQLiteDatabase实例,真的是层层转包啊 SQLiteDatabase sqliteDatabase = openDatabase(path, factory, flags, errorHandler,(short) 0); if(sBlockSize == 0) sBlockSize = newStatFs("/data").getBlockSize(); //为该SQLiteDatabase实例设置一些参数。这些内容和SQLite本身的特性有关,本书不 //拟深入讨论这方面的内容,感兴趣的读者不妨参考SQLite官网提供的资料 sqliteDatabase.setPageSize(sBlockSize); sqliteDatabase.setJournalMode(path, "TRUNCATE"); synchronized(mActiveDatabases) { mActiveDatabases.add( newWeakReference<SQLiteDatabase>(sqliteDatabase)); } returnsqliteDatabase; } ~~~ openDatabase将真正创建一个SQLiteDatabase实例,其相关代码是: **SqliteDatabase.java::openDatabase** ~~~ private static SQLiteDatabase openDatabase(Stringpath, CursorFactory factory, int flags,DatabaseErrorHandler errorHandler, shortconnectionNum) { //构造一个SQLiteDatabase实例 SQLiteDatabase db = new SQLiteDatabase(path, factory, flags,errorHandler, connectionNum); try { db.dbopen(path, flags);//打开数据库,dbopen是一个native函数 db.setLocale(Locale.getDefault());//设置Locale ...... returndb; }...... } ~~~ 其实openDatabase主要就干了两件事情,即创建一个SQLiteDatabase实例,然后调用该实例的dbopen函数。 (2) SQLiteDatabase的构造函数及dbopen函数分析 先看SQLitedDatabase的构造函数,代码如下: **SQLitedDatabase.java::SQLiteDatabas**e ~~~ private SQLiteDatabase(String path, CursorFactoryfactory, int flags, DatabaseErrorHandler errorHandler, short connectionNum) { setMaxSqlCacheSize(DEFAULT_SQL_CACHE_SIZE); mFlags =flags; mPath = path; mFactory= factory; mPrograms= new WeakHashMap<SQLiteClosable,Object>(); // config_cursorWindowSize值为2048,所以下面得到的limit值应该为8MB int limit= Resources.getSystem().getInteger( com.android.internal.R.integer.config_cursorWindowSize) * 1024 * 4; native_setSqliteSoftHeapLimit(limit); } ~~~ 前面说过,Java层的SQLiteDatabase对象会和一个Native层sqlite3实例绑定,从以上代码中可发现,绑定的工作并未在构造函数中开展。实际上,该工作是由dbopen函数完成的,其相关代码如下: **android_database_SQLiteDatabase.cpp::dbopen** ~~~ static void dbopen(JNIEnv* env, jobject object,jstring pathString, jint flags) { int err; sqlite3* handle = NULL; sqlite3_stmt * statement = NULL; charconst * path8 = env->GetStringUTFChars(pathString, NULL); intsqliteFlags; registerLoggingFunc(path8); if(flags & CREATE_IF_NECESSARY) { sqliteFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; } ...... //调用sqlite3_open_v2函数创建数据库,sqlite3_open_v2和示例中的sqlite3_open类似 //handle用于存储新创建的sqlite3*类型的实例 err =sqlite3_open_v2(path8, &handle, sqliteFlags, NULL); ...... sqlite3_soft_heap_limit(sSqliteSoftHeapLimit); err =sqlite3_busy_timeout(handle, 1000 /* ms */); ...... //Android在原生SQLite之上还做了一些特殊的定制,相关内容留待本节最后分析 err =register_android_functions(handle, UTF16_STORAGE); //将handle保存到Java层的SQLiteDatabase对象中,这样Java层SQLiteDatabase实例 //就和一个Native层的sqlite3实例绑定到一起了 env->SetIntField(object, offset_db_handle, (int) handle); handle =NULL; // The caller owns the handle now. done: if(path8 != NULL) env->ReleaseStringUTFChars(pathString, path8); if(statement != NULL) sqlite3_finalize(statement); if(handle != NULL) sqlite3_close(handle); } ~~~ 从上述代码可知,使用dbopen函数其实就是为了得到Native层的一个sqlite3实例。另外,Android对SQLite还设置了一些平台相关的函数,这部分内容将在后文进行分析。 3. SQLiteCompiledSql介绍 前文曾提到,Native层sqlite3_stmt实例的封装是由未对开发者公开的类SQLiteCompileSql完成的。由于它的隐秘性,没有在图7-4中把它列出来。现在我们就来揭开它神秘的面纱,其代码如下: **SQLiteCompliteSql.java::SQLiteCompiledSql** ~~~ SQLiteCompiledSql(SQLiteDatabase db, String sql) { db.verifyDbIsOpen(); db.verifyLockOwner(); mDatabase = db; mSqlStmt= sql; ...... nHandle= db.mNativeHandle; native_compile(sql);//调用native_compile函数,代码如下 } ~~~ **android_database_SQLiteCompliteSql.cpp::native_compile** ~~~ static void native_compile(JNIEnv* env, jobjectobject, jstring sqlString) { compile(env, object, GET_HANDLE(env, object), sqlString); } //来看compile的实现 sqlite3_stmt * compile(JNIEnv* env, jobjectobject, sqlite3 * handle,jstring sqlString) { int err; jcharconst * sql; jsizesqlLen; sqlite3_stmt * statement = GET_STATEMENT(env, object); if(statement != NULL) ....//释放之前的sqlite3_stmt实例 sql = env->GetStringChars(sqlString,NULL); sqlLen =env->GetStringLength(sqlString); //调用sqlite3_prepare16_v2得到一个sqlite3_stmt实例 err =sqlite3_prepare16_v2(handle, sql, sqlLen * 2, &statement, NULL); env->ReleaseStringChars(sqlString, sql); if (err== SQLITE_OK) { //保存到Java层的SQLiteCompliteSql对象中 env->SetIntField(object, gStatementField, (int)statement); return statement; } ...... } ~~~ 当compile函数执行完后,一个绑定了SQL语句的sqlite3_stmt实例就和Java层的SQLiteCompileSql对象绑定到一起了。 4. Android SQLite自定义函数介绍 本节将介绍Android在SQLite上自定义的一些函数。一切还得从SQL的触发器说起。 (1) 触发器介绍 触发器(Trigger)是数据库开发技术中一个常见的术语。其本质非常简单,就是在指定表上发生特定事情时,数据库需要执行的某些操作。还是有点模糊吧?再来看MediaProvider设置的一个触发器: ~~~ db.execSQL("CREATE TRIGGER IF NOT EXISTSimages_cleanup DELETE ON images " + "BEGIN " + "DELETE FROM thumbnails WHERE image_id = old._id;" + "SELECT _DELETE_FILE(old._data);" + "END"); ~~~ 上面这条SQL语句是什么意思呢? - CREATE TRIGGER IF NOT EXITSimages_cleanup:如果没有定义名为images_cleanup的触发器,就创建一个名为images_cleanup的触发器。 - DELETE ON images:设置该触发器的触发条件。显然,当我们对images表执行delete操作时,该触发器将被触发。 BEGIN和END之间则定义了该触发器要执行的动作。从前面的代码可知,它将执行两项操作: - 删除thumbnails(缩略图)表中对应的信息。为什么要删除缩略图呢?因为原图的信息已经不存在了,留着它没用。 - 执行_DELETE_FILE函数,其参数是old.data。从名字上来看,这个函数的功能应为删除文件。为什么要删除此文件?原因也很简单,数据库都没有该项信息了,还留着图片干什么!另外,如不删除文件,下一次媒体扫描时就又会把它们找到。 * * * * * **提示**:_DELETE_FILE这个操作曾给笔者及同仁带来极大困扰,因为最开始并不知道有这个触发器。结果好不容易下载的测试文件全部被删除了。另外,由于MediaProvider本身的设计缺陷,频繁挂/卸载SD卡时也会错误删除数据库信息(这个缺陷只能尽量避免,无法彻底根除),结果实体文件也被删除掉了。 * * * * * 有人可能会感到奇怪,这个_DELETE_FILE函数是谁设置的呢?答案就在前面提到的register_android_functions中。 (2) register_android_functions介绍 register_android_functions在dbopen中被调用,其代码如下: **sqlite3_android.cpp::register_android_functions** ~~~ //dbopen调用它时,第二个参数设置为0 extern "C" intregister_android_functions(sqlite3 * handle, int utf16Storage) { int err; UErrorCode status = U_ZERO_ERROR; UCollator * collator = ucol_open(NULL, &status); ...... if(utf16Storage) { err =sqlite3_exec(handle, "PRAGMA encoding = 'UTF-16'", 0, 0, 0); ...... } else { //sqlite3_create_collation_xx定义一个用于排序的文本比较函数,读者可自行阅读 //SQLite官方文档以获得更详细的说明 err = sqlite3_create_collation_v2(handle,"UNICODE", SQLITE_UTF8, collator, collate8, (void(*)(void*))localized_collator_dtor); } /* 调用sqlite3_create_function创建一个名为"PHONE_NUMBERS_EQUAL"的函数, 第三个参数2表示该函数有两个参数,SQLITE_UTF8表示字符串编码为UTF8, phone_numbers_equal为该函数对应的函数指针,也就是真正会执行的函数。注意 "PHONE_NUMBERS_EQUAL"是SQL语句中使用的函数名,phone_numbers_equal是Native 层对应的函数 */ err =sqlite3_create_function( handle, "PHONE_NUMBERS_EQUAL", 2, SQLITE_UTF8, NULL, phone_numbers_equal, NULL, NULL); ...... //注册_DELETE_FILE对应的函数为delete_file err =sqlite3_create_function(handle, "_DELETE_FILE", 1, SQLITE_UTF8, NULL, delete_file, NULL, NULL); if (err!= SQLITE_OK) { return err; } #if ENABLE_ANDROID_LOG err =sqlite3_create_function(handle, "_LOG", 1, SQLITE_UTF8, NULL, android_log,NULL, NULL); ...... #endif ......//和PHONE相关的一些函数 returnSQLITE_OK; } ~~~ register_android_functions注册了Android平台上定制的一些函数。来看和_DELETE_FILE有关的delete_file函数,其代码为: **Sqlite3_android.cpp::delete_file** ~~~ static void delete_file(sqlite3_context * context,int argc, sqlite3_value** argv) { if (argc!= 1) { sqlite3_result_int(context, 0); return; } //从argv中取出第一个参数,这个参数是触发器调用_DELETE_FILE时传递的 charconst * path = (char const *)sqlite3_value_text(argv[0]); ...... /* Android4.0之后,系统支持多个存储空间(很多平板都有一块很大的内部存储空间)。 为了保持兼容性,环境变量EXTERNAL_STORAGE还是指向sd卡的挂载目录,而其他存储设备的 挂载目录由SECCONDARY_STORAGE表示,各个挂载目录由冒号分隔。 下面这段代码用于判断_DELETE_FILE函数所传递的文件路径是不是正确的 */ boolgood_path = false; charconst * external_storage = getenv("EXTERNAL_STORAGE"); if(external_storage && strncmp(external_storage, path,strlen(external_storage)) == 0) { good_path = true; } else { charconst * secondary_paths = getenv("SECONDARY_STORAGE"); while (secondary_paths && secondary_paths[0]) { const char* colon = strchr(secondary_paths, ':'); int length = (colon ? colon - secondary_paths : strlen(secondary_paths)); if (strncmp(secondary_paths, path, length) == 0) { good_path = true; } secondary_paths += length; while (*secondary_paths == ':')secondary_paths++; } } if(!good_path) { sqlite3_result_null(context); return; } //调用unlink删除文件 int err= unlink(path); if (err!= -1) { sqlite3_result_int(context, 1);//设置返回值 } else { sqlite3_result_int(context, 0); } } ~~~ [^①]:其实这是一种广义的设计模式,读者可参考《Pattern-Oriented Software Architecture Volume 3: Patterns forResource Management》一书以加深理解。