7.3.2 MediaProvider创建数据库分析

在MediaProvider中触发数据库的是attach函数,其代码如下:

[—>MediaProvider:attach]


private Uri attachVolume(String volume){

Context context=getContext();

DatabaseHelper db;

if(INTERNAL_VOLUME.equals(volume)){

……//针对内部存储空间的数据库

}else if(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构造函数分析

DatabaseHelper构造函数的代码如下:

[—>MediaProvider.java:DatabaseHelper]


public DatabaseHelper(Context context, String name, 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, String name, CursorFactory factory,

int version){

//调用另外一个构造函数,注意它新建了一个默认的错误处理对象

this(context, name, factory, version, new DefaultDatabaseErrorHandler());

}

public SQLiteOpenHelper(Context context, String name, CursorFactory factory,

int version, DatabaseErrorHandler errorHandler){

……

mContext=context;

mName=name;

//看到”factory“一词,读者要能想到设计模式中的工厂模式,在本例中该变量为null

mFactory=factory;

mNewVersion=version;

mErrorHandler=errorHandler;

}


上面这些函数都比较简单,其中却蕴含一个较为深刻的设计理念,具体如下:从SQLiteOpenHelper的构造函数中可知,MediaProvider对应的数据库对象(即SQLiteDatabase实例)并不在该函数中创建。那么,代表数据库的SQLiteDatabase实例是何时创建呢?此处使用了所谓的延迟创建(lazy creation)的方法,即SQLiteDatabase实例真正创建的时间是在第一次使用它的时候,也就是本例中第二个关键点函数getWritableDatabase。

在分析getWritableDatabase函数之前,先介绍一些和延迟创建相关的知识。

延迟创建或延迟初始化(lazy intializtion)所谓的“重型”资源(如占内存较大或创建时间比较长的资源),是系统开发和设计中常用的一种策略[1]。在使用这种策略时,开发人员不仅在资源创建时“斤斤计较”,在资源释放的问题上也是“慎之又慎”。资源释放的控制一般会采用引用计数技术。

结合前面对SQLiteDatabase的介绍会发现,SQLiteDatabase这个框架,在设计时不是简单地将SQLite API映射到Java层,而是有大量更为细致的考虑。例如,在这个框架中,资源创建采用了lazy creation方法,资源释放又利用SQLiteClosable来控制生命周期。

建议 Android中的SQLiteDatabase框架虽更完善、更具扩展性,但是使用它比直接使用SQLite API要复杂得多,因此在开发过程中,应当根据实际情况综合考虑是否使用该框架。例如,笔者在开发公司的DLNA解决方案时,就直接使用了SQLite API,而没使用这个框架。

(2)getWritableDatabase函数分析

现在来看getWritableDatabase的代码,具体如下:

[—>SQLiteDatabase.java:getWritableDatabase]


public synchronized SQLiteDatabase getWritableDatabase(){

if(mDatabase!=null){

//第一次调用该函数时mDatabase还未创建。以后的调用将直接返回已经创建好的mDatabase

}

boolean success=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);

}

int version=db.getVersion();

if(version!=mNewVersion){

db.beginTransaction();

try{

if(version==0){

/*

如果初次创建该数据库(即对应的数据库文件不存在),则调用子类实现的

onCreate函数。子类实现的onCreate函数将完成数据库建表等操作。读者不妨

查看MediaProvider DatabaseHelper实现的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(String name, 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(String path, CursorFactory

factory, int flags, DatabaseErrorHandler errorHandler){

//又调用openDatabase创建SQLiteDatabase实例,真的是层层转包啊

SQLiteDatabase sqliteDatabase=openDatabase(path, factory,

flags, errorHandler,(short)0);

if(sBlockSize==0)sBlockSize=new StatFs("/data").getBlockSize();

//为该SQLiteDatabase实例设置一些参数。这些内容和SQLite本身的特性有关,本书不

//深入讨论这方面的内容,感兴趣的读者不妨参考SQLite官网提供的资料

sqliteDatabase.setPageSize(sBlockSize);

sqliteDatabase.setJournalMode(path,"TRUNCATE");

synchronized(mActiveDatabases){

mActiveDatabases.add(

new WeakReference<SQLiteDatabase>(sqliteDatabase));

}

return sqliteDatabase;

}


openDatabase将真正创建一个SQLiteDatabase实例,其相关代码是:

[—>SqliteDatabase.java:openDatabase]


private static SQLiteDatabase openDatabase(String path, CursorFactory factory,

int flags, DatabaseErrorHandler errorHandler,

short connectionNum){

//构造一个SQLiteDatabase实例

SQLiteDatabase db=new SQLiteDatabase(path, factory, flags, errorHandler,

connectionNum);

try{

db.dbopen(path, flags);//打开数据库,dbopen是一个native函数

db.setLocale(Locale.getDefault());//设置Locale

……

return db;

}……

}


其实openDatabase主要就干了两件事情,即创建一个SQLiteDatabase实例,然后调用该实例的dbopen函数。

(2)SQLiteDatabase的构造函数及dbopen函数分析

先看SQLitedDatabase的构造函数,代码如下:

[—>SQLitedDatabase.java:SQLiteDatabase]


private SQLiteDatabase(String path, CursorFactory factory, 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)

10244;

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;

char const*path8=env->GetStringUTFChars(pathString, NULL);

int sqliteFlags;

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实例的封装是由未对开发者公开的类SQLiteCompiledSql完成的。由于SQLiteCompiledSql的隐秘性,没有在图7-4中把它列出来。现在我们就来揭开它神秘的面纱,其代码如下:

[—>SQLiteCompiledSql.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_SQLiteCompiledSql.cpp:native_compile]


static void native_compile(JNIEnv*env, jobject object, jstring sqlString)

{

compile(env, object, GET_HANDLE(env, object),sqlString);

}

//下面来看compile的实现

sqlite3_stmtcompile(JNIEnvenv, jobject object,

sqlite3*handle, jstring sqlString)

{

int err;

jchar const*sql;

jsize sqlLen;

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层的SQLiteCompiledSql对象中

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 EXISTS images_cleanup DELETE ON images"+

"BEGIN"+

"DELETE FROM thumbnails WHERE image_id=old._id;"+

"SELECT_DELETE_FILE(old._data);"+

"END");


上面这条SQL语句是什么意思呢?

CREATE TRIGGER IF NOT EXITS images_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"int register_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相关的一些函数

return SQLITE_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时传递的

char constpath=(char const)sqlite3_value_text(argv[0]);

……

/*

Android 4.0之后,系统支持多个存储空间(很多平板都有一块很大的内部存储空间)。

为了保持兼容性,环境变量EXTERNAL_STORAGE还是指向SD卡的挂载目录,而其他存储设备的

挂载目录由SECCONDARY_STORAGE表示,各个挂载目录由冒号分隔开。

下面这段代码用于判断_DELETE_FILE函数所传递的文件路径是不是正确的

*/

bool good_path=false;

char const*external_storage=getenv("EXTERNAL_STORAGE");

if(external_storage&&strncmp(external_storage,

path, strlen(external_storage))==0){

good_path=true;

}else{

char const*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);

}

}


[1]其实这是一种广义的设计模式,读者可参考《Pattern-Oriented Software Architecture Volume 3:Patterns for  Resource Management》一书加深理解。