7.4.2 MediaProvider的query分析
本节将分析MediaProvider的query函数。此次分析的焦点不是MediaProvider本身的业务逻辑,而是要搞清query函数返回的Cursor到底是什么,其代码如下,:
[—>MediaProvider.java:query]
public Cursor query(Uri uri, String[]projectionIn, String selection,
String[]selectionArgs, String sort){
int table=URI_MATCHER.match(uri);
……
//根据uri取出对应的DatabaseHelper对象,MediaProvider针对内部存储中的媒体文件和
//外部存储(即SD卡)中的媒体文件分别创建了两个数据库
DatabaseHelper database=getDatabaseForUri(uri);
//我们在7.3.2节分析过getReadableDatabase函数的兄弟getWritableDatabase函数
SQLiteDatabase db=database.getReadableDatabase();
//创建一个SQLiteQueryBuilder对象,用于方便开发人员编写SQL语句
SQLiteQueryBuilder qb=new SQLiteQueryBuilder();
……//设置qb的参数,例如调用setTables函数为本次查询设定目标Table
//①调用SQLiteQueryBuilder的query函数
Cursor c=qb.query(db, projectionIn, selection,
combine(prependArgs, selectionArgs),groupBy, null, sort, limit);
if(c!=null){
//②调用该Cursor对象的setNotificationUri函数,这部分内容和ContentObserver
//有关,相关内容留到第8章再进行分析
c.setNotificationUri(getContext().getContentResolver(),uri);
}
return c;
}
上边代码列出的两个关键点分别是:
调用SQLiteQueryBuilder的query函数得到一个Cursor类型的对象。
调用Cursor类型对象的setNotificationUri函数。从名字上看,该函数是为Cursor对象设置通知URI。和ContentObserver有关的内容留到第8章再进行分析。
下面来看SQLiteQueryBuilder的query函数。
1.SQLiteQueryBuilder的query函数分析
这部分的代码如下:
[—>SQLiteQueryBuilder.java:query]
public Cursor query(SQLiteDatabase db, String[]projectionIn,
String selection, String[]selectionArgs, String groupBy,
String having, String sortOrder, String limit){
……
//调用buildQuery函数得到对应SQL语句的字符串
String sql=buildQuery(
projectionIn, selection, groupBy, having,
sortOrder, limit);
/*
调用SQLiteDatbase的rawQueryWithFactory函数,mFactory是SQLiteQueryBuilder
的成员变量,初始值为null,本例也没有设置它,故mFactory为null
*/
return db.rawQueryWithFactory(
mFactory, sql, selectionArgs,
SQLiteDatabase.findEditTable(mTables));
}
[—>SQLiteDatabase.java:rawQueryWithFactory]
public Cursor rawQueryWithFactory(
CursorFactory cursorFactory, String sql, String[]selectionArgs,
String editTable){
verifyDbIsOpen();
BlockGuard.getThreadPolicy().onReadFromDisk();
//数据库开发中经常碰到连接池的概念,其目的也是缓存重型资源。有兴趣的读者不妨自行
//研究一下面这个getDbConnection函数
SQLiteDatabase db=getDbConnection(sql);
//创建一个SQLiteDirectCursorDriver对象
SQLiteCursorDriver driver=new SQLiteDirectCursorDriver(
db, sql, editTable);
Cursor cursor=null;
try{
//调用SQLiteCursorDriver的query函数
cursor=driver.query(
cursorFactory!=null?cursorFactory:mFactory,
selectionArgs);
}finally{
releaseDbConnection(db);
}
return cursor;
以上代码中又出现一个新类型,即SQLiteCursorDriver, cursor变量是其query函数的返回值。
(1)SQLiteCursorDriver的query函数分析
SQLiteCursorDriver的query函数的代码如下:
[—>SQLiteCursorDriver.java:query]
public Cursor query(CursorFactory factory, String[]selectionArgs){
//本例中,factory为空
SQLiteQuery query=null;
try{
mDatabase.lock(mSql);
mDatabase.closePendingStatements();
//①构造一个SQLiteQuery对象
query=new SQLiteQuery(mDatabase, mSql,0,selectionArgs);
//原来,最后返回的游标对象其真实类型是SQLiteCursor
if(factory==null){//②构造一个SQLiteCursor对象
mCursor=new SQLiteCursor(this, mEditTable, query);
}else{
mCursor=factory.newCursor(mDatabase, this,
mEditTable, query);
}
mQuery=query;
query=null;
return mCursor;//原来返回的cursor对象其真实类型是SQLiteCursor
}finally{
if(query!=null)query.close();
mDatabase.unlock();
}
}
SQLiteCursorDriver的query函数的主要功能就是创建一个SQLiteQuery实例和一个SQLiteCursor实例。至此,我们终于搞清楚了MediaProvider的query返回的游标对象其真实类型是SQLiteCursor。
注意 这里的MediaProvider query指的是图7-5中的编号为8的关键调用。
下面来看SQLiteQuery和SQLiteCursor为何方神圣。
(2)SQLiteQuery介绍
在图7-4中曾介绍过SQLiteQuery,它保存了和查询(即SQL的SELECT命令)命令相关的信息,其构造函数的代码如下:
[—>SQLiteQuery. java:构造函数]
SQLiteQuery(SQLiteDatabase db, String query, int offsetIndex,
String[]bindArgs){
//注意,在本例中offsetIndex为0,offsetIndex的意义到后面再解释
super(db, query);//调用基类构造函数
mOffsetIndex=offsetIndex;
bindAllArgsAsStrings(bindArgs);
}
SQLiteQuery的基类是SQLiteProgram,其代码如下:
[—>SQLiteProgram. java:构造函数]
SQLiteProgram(SQLiteDatabase db, String sql){
this(db, sql, null, true);//调用另外一个构造函数,注意传递的参数
}
SQLiteProgram(SQLiteDatabase db, String sql, Object[]bindArgs,
boolean compileFlag){
//本例中bindArgs为null, compileFlag为true
mSql=sql.trim();
//返回该SQL语句的类型,query语句将返回STATEMENT_SELECT类型
int n=DatabaseUtils.getSqlStatementType(mSql);
switch(n){
case DatabaseUtils.STATEMENT_UPDATE:
mStatementType=n|STATEMENT_CACHEABLE;
break;
case DatabaseUtils.STATEMENT_SELECT:
/*
mStatementType成员变量用于标示该SQL语句的类型,如果该SQL语句
是STATEMENT_SELECT类型,则mStatementType将设置STATEMENT_CACHEABLE
标志。该标志表示此对象将被缓存起来,以避免再次执行同样的SELECT命令时重新构造
一个对象
*/
mStatementType=n|STATEMENT_CACHEABLE|
STATEMENT_USE_POOLED_CONN;
break;
……//其他情况处理
default:
mStatementType=n;
}
db.acquireReference();//增加引用计数
db.addSQLiteClosable(this);
mDatabase=db;
nHandle=db.mNativeHandle;//此mNativeHandle对应一个SQLite3实例
if(bindArgs!=null)……//绑定参数
//complieAndBindAllArgs将为此对象绑定一个sqlite3_stmt实例,native层对象的指针
//保存在nStatement成员变量中
if(compileFlag)compileAndbindAllArgs();
}
来看compileAndbindAllArgs函数,其代码是:
[—>SQLiteProgram.java:compileAndbindAllArgs]
void compileAndbindAllArgs(){
……
//如果该对象还未和native层的sqlite3_stmt实例绑定,则调用compileSql函数
if(nStatement==0)compileSql();
……//绑定参数
for(int index:mBindArgs.keySet()){
Object value=mBindArgs.get(index);
if(value==null){
native_bind_null(index);
}……//绑定其他类型的数据
}
}
compileSql函数将绑定Java层SQLiteQuery对象到一个Native的sqlite3_stmt实例。根据前文的分析,这个绑定是通过SQLiteCompileSql对象实施的,其相关代码如下:
[—>SQLiteProgram.java:compileSql]
private void compileSql(){
//如果mStatementType未设置STATEMENT_CACHEABLE标志,则每次都创建一个
//SQLiteCompiledSql对象。根据7.3.2节中的分析,该对象会真正和Native层的
//sqlite_stmt实例绑定
if((mStatementType&STATEMENT_CACHEABLE)==0){
mCompiledSql=new SQLiteCompiledSql(mDatabase, mSql);
nStatement=mCompiledSql.nStatement;
return;
}
//从SQLiteDatabase对象中查询是否已经缓存过符合该SQL语句要求的SQLiteCompiledSql
//对象
mCompiledSql=mDatabase.getCompiledStatementForSql(mSql);
if(mCompiledSql==null){
//创建一个新的SQLiteCompiledSql对象,并把它保存到mDatabase中
mCompiledSql=new SQLiteCompiledSql(mDatabase, mSql);
mCompiledSql.acquire();
mDatabase.addToCompiledQueries(mSql, mCompiledSql);
}……
//保存Native层sqlite3_stmt实例指针到nStatement成员变量
nStatement=mCompiledSql.nStatement;
}
通过以上分析可以发现,SQLiteQuery将和一个代表SELECT命令的sqlite3_stmt实例绑定。同时,为了减少创建sqlite3_stmt实例的开销,SQLiteDatabase框架还会把对应的SQL语句和对应的SQLiteCompiledSql对象缓存起来。如果下次执行同样的SELECT语句,那么系统将直接取出之前保存的SQLiteCompiledSql对象,这样就不用重新创建sqlite3_stmt实例了。
思考 与直接使用SQLite API相比,SQLiteDatabase框架明显考虑了更多问题。读者自己封装C++SQLite库时是否想到了这些问题?
(3)SQLiteCursor分析
再来看SQLiteCursor类,其构造函数的代码如下:
[—>SQLiteCursor. java:构造函数]
public SQLiteCursor(SQLiteCursorDriver driver, String editTable,
SQLiteQuery query){
……
mDriver=driver;
mEditTable=editTable;
mColumnNameMap=null;
mQuery=query;//保存此SQLiteQuery对象
query.mDatabase.lock(query.mSql);
try{
//得到此次执行query得到的结果集所包含的列数
int columnCount=mQuery.columnCountLocked();
mColumns=new String[columnCount];
for(int i=0;i<columnCount;i++){
String columnName=mQuery.columnNameLocked(i);
//保存列名
mColumns[i]=columnName;
if("_id".equals(columnName)){
mRowIdColumnIndex=i;//保存“_id”列在结果集中的索引位置
}
}
}finally{
query.mDatabase.unlock();
}
}
SQLiteCursor比较简单,此处不再详述。
2.Cursor分析
至此,我们已经知道MediaProvider query返回的游标对象的真实类型了。现在,终于可以请Cursor家族登台亮相了,如图7-6所示。
图 7-6 Cursor家族
图7-6中元素较多,包含的知识点也较为复杂,因此必须仔细阅读下文的解释。
通过7.3.1节中SqliteTest示例可知,query查询(即SELECT命令)的结果和一个Native sqlite_stmt实例绑定在一起,开发者可通过遍历该sqlite_stmt实例得到自己想要的结果(例如,调用sqlite3_step遍历结果集中的行,然后通过sqlite3_column_xxx取出指定列的值)。查询结果集可通过数据库开发技术中一个专用术语——游标(Cursor)来遍历和获取。图7-6的左上部分是和Cursor有关的类,它们包括:接口类Cursor和CrossProcessCursor、抽象类AbstractCursor、AbstractWindowCursor,以及真正的实现类SQLiteCursor。根据前面的分析,SQLiteCursor内部保存一个已经绑定了sqlite3_stmt实例的SQLiteQuery对象,故读者可简单地把SQLiteCursor看成是一个已经包含了查询结果集的游标对象,虽然此时还未真正执行SQL语句。
如上所述,SQLiteCursor是一个已经包含了结果集的游标对象。从进程角度看,query的结果集目前还属于MediaProvider所在的进程,而本次query请求是由客户端发起的,所以一定要有一种方法将MediaProvider中的结果集传递到客户端进程。数据传递使用的技术很简单,就是大家耳熟能详的共享内存技术。SQLite API没有提供相关的功能,但是SQLiteDatabase框架对跨进程数据传递进行了封装,最终得到了图7-6左上部分的CursorWindow类。其代码中的注释明确表明了CursorWindow的作用,它是“A buffer containing multiple cursor rows”。
认识了CursorWindow类后,相信读者就能猜出query中数据传递的大致流程了。
MediaProvider将结果集中的数据存储到CursorWindow的共享内存中,然后客户端将其从共享内存中取出来即可。
上述流程描述是对的,但实际过程并非如此简单,因为SQLiteDatabase框架希望客户端看到的不是共享内存,而是一个代表结果集的游标对象,就好像客户端查询的是本进程中的数据库一样。由于存在这种要求[1],Android构造了图7-6右下角的类家族。其中,最重要的两个类是CursorToBulkCursorAdaptor和BulkCursorToCursorAdatpor。从名字上看,它们采用了设计模式中的Adaptor模式;从继承关系上看,这两个类将参与跨进程的Binder通信(其中客户端使用的BulkCursorToCursorAdaptor通过mBulkCursor与位于MediaProvider所在进程的CursorToBulkCursorAdaptor通信)。这两个类中最重要的是onMove函数,以后我们碰到时再作分析。
另外,图7-6中右上角部分展示了CursorWrapperInner类的派生关系。CursorWrapper-Inner类是ContentResolver query函数最终返回给客户端的游标对象的类型。CursorWrapper-Inner的目的应该是拓展CursorToBulkCursorAdaptor类的功能。
Cursor家族有些复杂。笔者觉得,目前对Cursor的架构设计有些过度(over-designed)。这不仅会导致我们分析时困难重重,并且会对实际代码的运行效率造成一定损失。
下面我们将焦点放到跨进程的数据传输上。
[1]此处应该还存在其他方面的设计考虑,希望读者能参与讨论。