7.4.3 query关键点分析
本节将按如下顺序分析query函数中的关键点:
介绍服务端的CursorToBulkCursorAdaptor及其count函数。
跨进程共享数据的关键类CursorWindow。
客户端的BulkCursorToCursorAdaptor及其initialize函数,以及返回给客户端使用的CursorWrapperInner类
1.CursorToBulkCursorAdaptor函数分析
(1)构造函数分析
CursorToBulkCursorAdaptor构造函数的代码如下:
[—>CursorToBulkCursorAdaptor. java:构造函数]
public CursorToBulkCursorAdaptor(Cursor cursor, IContentObserver observer,
String providerName){
//传入的cursor变量其真实类型是SQLiteCursor,它是CrossProcessCursor
if(cursor instanceof CrossProcessCursor){
mCursor=(CrossProcessCursor)cursor;
}else{
mCursor=new CrossProcessCursorWrapper(cursor);
}
mProviderName=providerName;
synchronized(mLock){//和ContentObserver有关,我们以后再作分析
createAndRegisterObserverProxyLocked(observer);
}
}
CursorToBulkCursorAdaptor的构造函数很简单,此处不详述。来看下一个重要函数,即CursorToBulkCursorAdaptor的count。该函数返回本次查询结果集所包含的行数。
(2)count函数分析
count函数的代码如下:
[—>CursorToBulkCursorAdaptor.java:count]
public int count(){
synchronized(mLock){
throwIfCursorIsClosed();//如果mCursor已经关闭,则抛出异常
//CursorToBulkCursorAdaptor的mCursor变量的真实类型是SQLiteCursor
return mCursor.getCount();
}
}
count最终将调用SQLiteCursor的getCount函数,其代码如下:
[—>SQLiteCursor.java:getCount]
public int getCount(){
if(mCount==NO_COUNT){//NO_COUNT为-1,首次调用时满足if条件
fillWindow(0);//关键函数
}
return mCount;
}
getCount函数将调用一个非常重要的函数,即fillWindow。顾名思义,读者可以猜测到它的功能:将结果数据保存到CursorWindow的那块共享内存中。
下面单起一节来分析和CursorWindow相关的知识点。
2.CursorWindow分析
CursorWindow的创建源于前边代码中对fillWindow的调用。fillWindow的代码如下:
[—>SQLiteCurosr.java:fillWindow]
private void fillWindow(int startPos){
//①如果CursorWinodow已经存在,则清空(clear)它,否则新创建一个
//CursorWinodow对象
clearOrCreateLocalWindow(getDatabase().getPath());
mWindow.setStartPosition(startPos);
//②getQuery返回一个SQLiteQuery对象,此处将调用它的fillWindow函数
int count=getQuery().fillWindow(mWindow);
if(startPos==0){
mCount=count;
}……
}
下面先来看clearOrCreateLocalWindow函数。
(1)clearOrCreateLocalWindow函数分析
[—>SQLiteCursor. java:clearOrCreateLocalWindow]
protected void clearOrCreateLocalWindow(String name){
if(mWindow==null){
mWindow=new CursorWindow(name, true);//创建一个CursorWindow对象
}else mWindow.clear();//清空CursorWindow中的信息
}
CursorWindow的构造函数的代码如下:
[—>CursorWindow. java:CursorWindow]
public CursorWindow(String name, boolean localWindow){
mStartPos=0;//本次查询的起始行位置,例如查询数据库表中第10到第100行的结果,
//其起始行就是10
/*
调用nativeCreate函数,注意传递的参数,其中sCursorWindowSize为2MB, localWindow
为true。sCursorWindowSize是一个静态变量,其值取自frameworks/base/core/res/res
/values/config.xml中定义的config_cursorWindowSize变量,该值是2048KB,而
sCursorWindow在此基础上扩大了1024倍,最终的结果就是2MB
*/
mWindowPtr=nativeCreate(name, sCursorWindowSize, localWindow);
mCloseGuard.open("close");
recordNewWindow(Binder.getCallingPid(),mWindowPtr);
}
nativeCreate是一个native函数,其真正实现在android_database_CursorWindow.cpp中,其代码如下:
[—>android_database_CursorWindow.cpp:nativeCreate]
static jint nativeCreate(JNIEnv*env, jclass clazz,
jstring nameObj, jint cursorWindowSize, jboolean localOnly){
String8 name;
if(nameObj){
const char*nameStr=env->GetStringUTFChars(nameObj, NULL);
name.setTo(nameStr);
env->ReleaseStringUTFChars(nameObj, nameStr);
}
……
CursorWindow*window;
//创建一个Native层的CursorWindow对象
status_t status=CursorWindow:create(name, cursorWindowSize,
localOnly,&window);
……
return reinterpret_cast<jint>(window);//将指针转换成jint类型
}
不妨再看看CursorWindow的create函数,其代码如下:
[—>CursorWindow. cpp:create]
status_t CursorWindow:create(const String8&name, size_t size, bool localOnly,
CursorWindow**outCursorWindow){
String8 ashmemName("CursorWindow:");
ashmemName.append(name);
ashmemName.append(localOnly?"(local)":"(remote)");
status_t result;
//创建共享内存,调用Android平台提供的ashmem_create_region函数
int ashmemFd=ashmem_create_region(ashmemName.string(),size);
if(ashmemFd<0){
result=-errno;
}else{
result=ashmem_set_prot_region(ashmemFd, PROT_READ|PROT_WRITE);
if(result>=0){
//映射共享内存以得到一块地址,data变量指向该地址的起始位置
void*data=:mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_SHARED, ashmemFd,0);
……
result=ashmem_set_prot_region(ashmemFd, PROT_READ);
if(result>=0){
//创建一个CursorWindow对象
CursorWindow*window=new CursorWindow(name, ashmemFd,
data, size, false);
result=window->clear();
if(!result){
*outCursorWindow=window;
return OK;//创建成功
}
}……//出错处理
}
return result;
}
由以上代码可知,CursorWindow的create函数将构造一个Native的CursorWindow对象。最终,Java层的CursorWindow对象会和此Native的CursorWindow对象绑定。
提示 CursorWindow创建中涉及共享内存方面的知识,读者可上网查询或阅读卷I的7.2.2节。
至此,用于承载数据的共享内存已创建完毕,但我们还没有执行SQL的SELECT语句。这个工作由SQLiteQuery的fillWindow函数完成。
(2)SQLiteQuery fillWindow分析
前面曾说过,SQLiteQuery保存了一个Native层的sqlite3_stmt实例,那么它的fillWindow函数是否就是执行SQL语句后将结果信息填充到CursorWindow中了呢?可以通过以下代码来验证。
[—>SQLiteQuery. java:fillWindow]
int fillWindow(CursorWindow window){
mDatabase.lock(mSql);
long timeStart=SystemClock.uptimeMillis();
try{
acquireReference();//增加一次引用计数
try{
window.acquireReference();
/*
调用nativeFillWindow函数。其中,nHandle指向Native层的sqlite3实例,
nStatement指向Native层的sqlite3_stmt实例,window.mWindowPtr指向
Native层的CursorWindow实例,该函数最终返回这次SQL语句执行后得到的结果
集中的记录项个数。mOffsetIndex参数的解释见下文
*/
int numRows=nativeFillWindow(nHandle,
nStatement, window.mWindowPtr,
window.getStartPosition(),mOffsetIndex);
mDatabase.logTimeStat(mSql, timeStart);
return numRows;
}……finally{
window.releaseReference();
}
}finally{
releaseReference();
mDatabase.unlock();
}
}
mOffsetIndex和SQL语句的OFFSET参数有关,可通过一条SQL语句来认识它。
SELECT*FROM IMAGES LIMIT 10 OFFSET 1
//上面这条SQL语句的意思是从IMAGES表中查询10条记录,这10条记录的起始位置为第1条。
//也就是查询第1到第11条记录
来看nativeFillWindow的实现函数,其代码是:
[—>android_database_SQLiteQuery. cpp:nativeFillWindow]
static jint nativeFillWindow(JNIEnv*env, jclass clazz, jint databasePtr,
jint statementPtr, jint windowPtr, jint startPos, jint offsetParam){
//取出Native层的实例
sqlite3database=reinterpret_cast<sqlite3>(databasePtr);
sqlite3_stmtstatement=reinterpret_cast<sqlite3_stmt>(statementPtr);
CursorWindowwindow=reinterpret_cast<CursorWindow>(windowPtr);
if(offsetParam>0){
//如果设置了查询的OFFSET,则需要为其绑定起始行。根据下面的设置,读者能
//推测出未绑定具体值的SQL语句吗?答案是:
//SELECT*FROM TABLE OFFSET,其中,offsetParam指明是第几个通配符,
//startPos用于绑定到这个通配符
int err=sqlite3_bind_int(statement, offsetParam, startPos);
}……
//计算本次查询(query)返回的结果集的列数
int numColumns=sqlite3_column_count(statement);
//将SQL执行的结果保存到CursorWindow对象中
status_t status=window->setNumColumns(numColumns);
……
int retryCount=0;
int totalRows=0;
int addedRows=0;
bool windowFull=false;
bool gotException=false;
//是否遍历所有结果
const bool countAllRows=(startPos==0);
//注意下面这个循环,它将遍历SQL的结果集,并将数据取出来保存到CursorWindow对象中
while(!gotException&&(!windowFull||countAllRows)){
int err=sqlite3_step(statement);
if(err==SQLITE_ROW){
retryCount=0;
totalRows+=1;
//windowFull变量用于标示CursorWindow是否还有足够内存。从前面的介绍可知,
//一个CursorWindow只分配了2MB的共享内存空间
if(startPos>=totalRows||windowFull){
continue;
}
//在共享内存中分配一行空间用于存储这一行的数据
status=window->allocRow();
if(status){
windowFull=true;//CursorWindow已经没有空间了
continue;
}
for(int i=0;i<numColumns;i++){
//获取这一行记录项中各列的值
int type=sqlite3_column_type(statement, i);
if(type==SQLITE_TEXT){
//如果这列中存储的是字符串,则将其取出来并通过CursorWindow的
//putString函数保存到共享内存中
const chartext=reinterpret_cast<const char>(
sqlite3_column_text(statement, i));
size_t sizeIncludingNull=sqlite3_column_bytes(statement, i)
+1;
status=window->putString(addedRows, i,text,
sizeIncludingNull);
if(status){
windowFull=true;
break;//CursorWindow没有足够的空间
}
}……//处理其他数据类型
}
if(windowFull||gotException){
window->freeLastRow();
}else{
addedRows+=1;
}
}else if(err==SQLITE_DONE){
……//结果集中所有行都遍历完
break;
}else if(err==SQLITE_LOCKED||err==SQLITE_BUSY){
//如果数据库正因为其他操作而被锁住,此处将尝试等待一段时间
if(retryCount>50){//最多等50次,每次1秒
throw_sqlite3_exception(env, database,"retrycount exceeded");
gotException=true;
}else{
usleep(1000);
retryCount++;
}
}……
}
//重置sqlite3_stmt实例,以供下次使用
sqlite3_reset(statement);
……//返回结果集中的行数
return countAllRows?totalRows:0;
}
通过以上代码可确认,fillWindow函数实现的就是将SQL语句的执行结果填充到了CursorWindow的共享内存中。读者如感兴趣,不妨研究一下CursorWindow是如何保存结果信息的。
建议 笔者在做网络开发时常做的一件事情就是将自定义的一些类实例对象序列化到一块内存中,然后将这块内存的内容通过socket发送给一个远端进程,而远端进程再将收到的数据反序列化以得到一个实例对象。通过这种方式,远端进程就得到了一个来自发送端的实例对象。读者不妨自学序列化/反序列化相关的知识。
(3)CursorWindow分析总结
本节向读者介绍了CursorWindow相关的知识点。其实,CursorWindow就是对一块共享内存的封装。另外我们也看到了如何将执行SELECT语句后得到的结果集填充到这块共享内存中。但是这块内存现在还仅属于服务端进程,只有客户端进程得到这块内存后,客户端才能真正获取执行SELECT后的结果。那么,客户端是何时得到这块内存的呢?让我们回到客户端进程。
3.BulkCursorToCursorAdaptor和CursorWrapperInner分析
客户端的工作是先创建BulkCursorToCursorAdaptor,然后根据远端查询(query)的结果调用BulkCursorToCursorAdaptor的intialize函数。
[—>BulkCursorToCursorAdaptor.java]
public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor{
private static final String TAG="BulkCursor";
//mObserverBridge和ContentOberver有关,我们留到7.5节再分析
private SelfContentObserver mObserverBridge=new SelfContentObserver(this);
private IBulkCursor mBulkCursor;
private int mCount;
private String[]mColumns;
private boolean mWantsAllOnMoveCalls;
//initialize函数
public void initialize(IBulkCursor bulkCursor, int count, int idIndex,
boolean wantsAllOnMoveCalls){
mBulkCursor=bulkCursor;
mColumns=null;
mCount=count;
mRowIdColumnIndex=idIndex;
mWantsAllOnMoveCalls=wantsAllOnMoveCalls;//该值为false
}
……
}
由以上代码可知,BulkCursorToCursorAdaptor仅简单保存了来自远端的信息,并没有什么特殊操作。看来客户端进程没有在上面代码的执行过程中共享内存。该工作会不会由CursorWrapperInner来完成呢?看ContentResolver query最终返回给客户端的对象的类CursorWrapperInner,其代码也较简单。
[—>ContentResolver. java:CursorWrapperInner]
private final class CursorWrapperInner extends CursorWrapper{
private final IContentProvider mContentProvider;
public static final String TAG="CursorWrapperInner";
/*
CloseGuard类是Android dalvik虚拟机提供的一个辅助类,用于帮助开发者判断
使用它的类的实例对象是否被显式关闭(close)。例如,假设有一个CursorWrapperInner
对象,当没有地方再引用它时,其finalize函数将被调用。如果之前没有调用过
CursorWrapperInner的close函数,那么finalize函数CloseGuard的warnIsOpen
将打印警告信息:"A resource was acquired at attached stack trace but never
released.See java.io.Closeable for information on avoiding resource
leaks."。感兴趣的读者可自行研究CloseGuard类
*/
private final CloseGuard mCloseGuard=CloseGuard.get();
private boolean mProviderReleased;
CursorWrapperInner(Cursor cursor, IContentProvider icp){
super(cursor);//调用基类的构造函数,其内部会将cursor变量保存到mCursor中
mContentProvider=icp;
mCloseGuard.open("close");
}
……
}
CursorWrapperInner的构造函数也没有去获取共享内存。别急,先看看执行query后的结果。
客户端通过Image.Media query函数,将得到一个CursorWrapperInner类型的游标对象。当然,客户端并不知道这么重要的细节,它只知道自己用的是接口类Cursor。根据前面的分析,此时客户端通过这个游标对象可与服务端的CursorToBulkCursorAdaptor交互,即进程间Binder通信的通道已经打通。但是此时客户端还未拿到那块至关重要的共享内存,即进程间的数据通道还没打通。那么,数据通道是何时打通的呢?
数据通道打通的时间和lazy creation有关,即只在使用它时才打通。
4.moveToFirst函数分析
据前文的分析,客户端从Image.Media query函数得到的游标对象,其真实类型是CursorWrapperInner。游标对象的使用有一个特点,即必须先调用它的move家族的函数。这个家族包括moveToFirst、moveToLast等函数。为什么一定要调用它们呢?来分析最常见的moveToFirst函数,该函数实际上由CursorWrapperInner的基类CursorWrapper来实现,代码如下:
[—>CursorWrapper. java:moveToFirst]
public boolean moveToFirst(){
//mCursor指向BulkCursorToCursorAdaptor
return mCursor.moveToFirst();
}
mCursor成员变量的真实类型是BulkCursorToCursorAdaptor,但其moveToFirst函数却是该类的“老祖宗”AbstractCursor实现的,代码如下:
[—>AbstractCursor. java:moveToFirst]
public final boolean moveToFirst(){
return moveToPosition(0);//调用moveToPosition,直接来看该函数
}
//moveToPosition的参数position表示将移动游标到哪一行
public final boolean moveToPosition(int position){
//getCount返回结果集中的行数,这个值在搭建Binder通信通道时,已经由服务端计算并返回
//给客户端了
final int count=getCount();
//mPos变量记录了当前游标的位置,该变量初值为-1
if(position>=count){
mPos=count;
return false;
}
if(position<0){
mPos=-1;
return false;
}
if(position==mPos)return true;
//onMove函数为抽象函数,由子类实现
boolean result=onMove(mPos, position);
if(result==false)mPos=-1;
else{
mPos=position;
if(mRowIdColumnIndex!=-1){
mCurrentRowID=Long.valueOf(getLong(mRowIdColumnIndex));
}
}
return result;
}
在上边代码中,moveToPosition将调用子类实现的onMove函数。在本例中,子类就是BulkCursorToCursorAdaptor,接下来看它的onMove函数。
(1)BulkCursorToCursorAdaptor的onMove函数分析
onMove函数的代码如下:
[—>BulkCursorToCursorAdaptor.java:onMove]
public boolean onMove(int oldPosition, int newPosition){
throwIfCursorIsClosed();
try{
//mWindow的类型就是CursorWindow。第一次调用该函数,mWindow为null
if(mWindow==null
||newPosition<mWindow.getStartPosition()
||newPosition>=mWindow.getStartPosition()+
mWindow.getNumRows()){
/*
mBulkCurosr用于和位于服务端的IBulkCursor Bn端通信,其getWindow函数
将返回一个CursorWindow类型的对象。也就是说,调用完getWindow函数后,
客户端进程就得到了一个CursorWindow,从此,客户端和服务端之间的数据通道就
打通了
*/
setWindow(mBulkCursor.getWindow(newPosition));
}else if(mWantsAllOnMoveCalls){
mBulkCursor.onMove(newPosition);
}
}……
if(mWindow==null)return false;
return true;
}
建立数据通道的关键函数是IBulkCurosr的getWindow。对于客户端而言,IBulkCursor Bp端对象的类型是BulkCursorProxy,下面介绍它的getWindow函数。
(2)BulkCursorProxy的getWindow函数分析
该getWindow函数的代码如下:
[—>BulkCursorNative.java:BulkCursorProxy:getWindow]
public CursorWindow getWindow(int startPos)throws RemoteException
{
Parcel data=Parcel.obtain();
Parcel reply=Parcel.obtain();
try{
data.writeInterfaceToken(IBulkCursor.descriptor);
data.writeInt(startPos);
mRemote.transact(GET_CURSOR_WINDOW_TRANSACTION, data, reply,0);
DatabaseUtils.readExceptionFromParcel(reply);
CursorWindow window=null;
if(reply.readInt()==1){
/*
根据服务端reply包构造一个本地的CursorWindow对象,读者可自行研究
newFromParcel函数,其内部会调用nativeCreateFromParcel函数以创建
一个Native的CursorWindow对象。整个过程就是笔者在前面提到的反序列化过程
*/
window=CursorWindow.newFromParcel(reply);
}
return window;
}……
}
再来看IBulkCursor Bn端的getWindow函数,此Bn端对象的真实类型是CursorToBulk-CursorAdaptor。
(3)CursorToBulkCursorAdaptor的getWindow函数分析
该getWindow函数的代码如下:
[—>CursorToBulkCursorAdaptor. java:getWindow]
public CursorWindow getWindow(int startPos){
synchronized(mLock){
throwIfCursorIsClosed();
CursorWindow window;
//mCursor是MediaProvider query返回的值,其真实类型是SQLiteCursor,满足
//下面的if条件
if(mCursor instanceof AbstractWindowedCursor){
AbstractWindowedCursor windowedCursor=
(AbstractWindowedCursor)mCursor;
//对于本例而言,SQLiteCursor已经和一个CursorWindow绑定了,所以window的值
//不为空
window=windowedCursor.getWindow();
if(window==null){
window=new CursorWindow(mProviderName, false);
windowedCursor.setWindow(window);
}
//调用SQLiteCursor的moveToPosition函数,该函数前面已经分析过了,在其
//内部将触发onMove函数的调用,此处将是SQLiteCursor的onMove函数
mCursor.moveToPosition(startPos);
}else{
……
}
if(window!=null){
window.acquireReference();
}
return window;
}
}
服务端返回的CursorWindow对象正是之前在count函数中创建的那个CursorWindow对象,其内部已经包含了执行本次query的查询结果。
另外,在将服务端的CursorWindow传递到客户端之前,系统会调用CursorWindow的writeToParcel函数进行序列化工作。读者可自行阅读CursorWindow的writeToParcel及nativeWriteToParcel函数。
(4)SQLiteCursor的moveToPostion函数分析
该函数由SQLiteCursor的基类AbstractCursor实现。我们前面已经看过它的代码了,其内部的主要工作就是调用AbstractCursor子类(此处就是SQLiteCursor自己)实现onMove函数,因此可直接看SQLiteCursor的onMove函数。
[—>SQLiteCursor. java:onMove]
public boolean onMove(int oldPosition, int newPosition){
if(mWindow==null||newPosition<mWindow.getStartPosition()||
newPosition>=(mWindow.getStartPosition()+
mWindow.getNumRows())){
fillWindow(newPosition);
}
return true;
}
以上代码中的if判断很重要,具体解释如下:
当mWindow为空,即服务端未创建CursorWindow时(当然,就本例而言,CursorWindow早已在query时就创建好了),需调用fillWindow。该函数内部将调用clearOrCreateLocalWindow。如果CursorWindow不存在,则创建一个CursorWindow对象。如果已经存在,则清空CursorWindow对象的信息。
当newPosition小于上一次查询得到的CursorWindow的起始位置,或者newPosition大于上一次查询得到的CursorWindow的最大行位置,也需调用fillWindow。由于此时CursorWindow已经存在,则clearOrCreateLocalWindow会调用它的clear函数以清空之前保存的信息。
调用fillWindow后将执行SQL语句,以获得正确的结果集。例如,假设上次执行query时设置了查询是从第10行开始的90条记录(即10~100行的记录),那么,当新的query若指定了从0行开始或从101行开始时,就需重新调用fillWindow,即将新的结果填充到CursorWindow中。如果新query查询的行数位于10~100之间,则无需再次调用fillWindow了。
这是服务端针对query做的一些优化处理,即当CursorWindow已经包含了所要求的数据时,就没有必要再次查询了。按理说,客户端也应该做类似的判断,以避免发起不必要的Binder请求。我们回过头来看客户端BulkCursorToCursorAdaptor的onMove函数。
[—>BulkCursorToCursorAdaptor. java:onMove]
public boolean onMove(int oldPosition, int newPosition){
throwIfCursorIsClosed();
try{
//同样,客户端也做了对应的优化处理,如果不满足if条件,客户端根本无须调用
//mBulkCurosr的getWindow函数,这样服务端也就不会收到对应的Binder请求了
if(mWindow==null
||newPosition<mWindow.getStartPosition()
||newPosition>=mWindow.getStartPosition()+
mWindow.getNumRows()){
setWindow(mBulkCursor.getWindow(newPosition));
)
……
}
(5)moveToFirst函数分析总结
moveToFirst及其相关的兄弟函数(如moveToLast和move等)的目的是移动游标位置到指定行。通过上面的代码分析,我们发现它的工作其实远不止移动游标位置这么简单。对于还未拥有CursorWindow的客户端来说,moveToFirst将导致客户端反序列化来自服务端的CursorWindow信息,从而使客户端和服务端之间的数据通道真正建立起来。