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》一书加深理解。