7.2.2 MediaStore.Image.Media的query函数分析

第二个关键点是在MediaProvider客户端示例中所调用的MediaStore.Image.Media的query函数。MediaStore是多媒体开发中常用的类,其

内部定义了专门针对Image、Audio、Video等不同多媒体信息的内部类来帮助客户端开发人员更好地和MediaProvider交互。这些类及相互之间的关系如图7-1所示。

7.2.2 MediaStore.Image.Media的query函数分析 - 图1

图 7-1 MediaStore类图

由图7-1可知,MediaStore定义了较多的内部类,我们重点展示作为内部类之一的Image的情况,其中:

MediaColumns定义了所有与媒体相关的数据库表都会用到的数据库字段,而ImageColumns定义了单独针对Image的数据库字段。

Image类定义了一个名为Media的内部类用于查询和Image相关的信息,同时Image类还定义了一个名为Thumbnails的内部类用于查询和Image相关的缩略图的信息(在Android平台上,缩略图的来源有两种,一种是Image,另一种是Video,故Image定义了名为Thumbnails的内部类,而Video也定义了一个名为Thumbnails的内部类)。

提示 MediaStore类较为复杂,主要原因是它定义了一些同名类。读者阅读代码时务必仔细。

下面来看Image.Media的query函数,其代码非常简单,如下所示:

[—>MediaStore.java:Image.Media.query]


public static final class Media implements ImageColumns{

public static final Cursor query(ContentResolver cr, Uri uri,

String[]projection){

//直接调用ContentResolver的query函数

return cr.query(uri, projection, null,

null, DEFAULT_SORT_ORDER);

}


Image. Media的query函数直接调用CR的query函数,虽然CR的真实类型是Application-ContentResolver,但是此函数却由其基类CR实现。

提示 追求运行效率的程序员也许会对上边这段代码的实现颇有微词,因为Image.Media的query函数基本上没做任何有意义的工作。相比客户端直接调用cr.query函数,此处的query就增加了一次函数调用和返回的开销(即Image.Media query调用和返回时参数的入栈/出栈)。但是,通过Image.Media的封装将使程序更清晰和易读(与直接使用CR的query相比,代码阅读者一看Image.Media就知道其query函数应该和Image有关,否则需要通过解析uri参数才能确定查询的信息是什么)。代码清晰易读和运行效率高,往往是软件开发中的熊掌和鱼,它们之间的对立性,将在本章中体现得淋漓尽致。笔者建议读者在实际开发中结合具体情况决定取舍,万不可钻牛角尖。

1.ContentResolver的query函数分析

这部分的代码如下:

[—>ContentResolver.java:query]


public final Cursor query(Uri uri, String[]projection,

String selection, String[]selectionArgs, String sortOrder){

//调用acquireProvider函数,参数为uri,函数也由CR实现

IContentProvider provider=acquireProvider(uri);

//注意,下面将和CP交互,相关知识留待7.4节再分析

……

}


CR的query将调用acquireProvider,该函数定义在CR类中,代码如下:

[—>ContentResolver.java:query]


public final IContentProvider acquireProvider(Uri uri){

if(!SCHEME_CONTENT.equals(uri.getScheme()))return null;

String auth=uri.getAuthority();

if(auth!=null){

//acquireProvider是一个抽象函数,由ContentResolver的子类实现。在本例中,该函数

//将由ApplicationContentResolver实现。uri.getAuthority将返回代表目标

//CP的名字

return acquireProvider(mContext, uri.getAuthority());

}

return null;

}


如上所述,acquireProvider由CR的子类实现,在本例中该函数由ApplicationContent-Resolver定义,代码如下:

[—>ContextImpl.java:acquireProvider]


protected IContentProvider acquireProvider(Context context, String name){

//mMainThread指向代表应用进程主线程的ActivityThread对象,每个应用进程只有一个

//ActivityThread对象

return mMainThread.acquireProvider(context, name);

}


如以上代码所示,最终ActivityThread的acquireProvider函数将被调用,希望它不要再被层层转包了。

2.AcitvityThread的acquireProvider函数分析

ActivityThread的acquireProvider函数的代码如下:

[—>ActivityThread.java:acquireProvider]


public final IContentProvider acquireProvider(Context c, String name){

//①调用getProvider函数,它很重要,具体见下文分析

IContentProvider provider=getProvider(c, name);

……

IBinder jBinder=provider.asBinder();

synchronized(mProviderMap){

//客户端进程将本进程使用的CP信息保存到mProviderRefCountMap中,

//其主要功能与引用计数和资源释放有关,读者暂可不理会它

ProviderRefCount prc=mProviderRefCountMap.get(jBinder);

if(prc==null)

mProviderRefCountMap.put(jBinder, new ProviderRefCount(1));

else prc.count++;

}

return provider;

}


在acquireProvider内部调用getProvider得到一个IContentProvider类型的对象,该函数非常重要,其代码为:

[—>ActivityThread.java:getProvider]


private IContentProvider getProvider(Context context, String name){

/*

查询该应用进程是否已经保存了用于和远端CP通信的对象existing。

此处,我们知道existing的类型是IContentProvider,不过IContentProvider是一

个interface,那么existing的真实类型是什么呢?稍后再揭示

*/

IContentProvider existing=getExistingProvider(context, name);

if(existing!=null)return existing;//如果existing存在,则直接返回

IActivityManager.ContentProviderHolder holder=null;

try{

//如果existing不存在,则需要向AMS查询,返回值的类型为ContentProviderHolder

holder=ActivityManagerNative.getDefault().getContentProvider(

getApplicationThread(),name);

}……

//注意:记住下面这个函数调用,此时是在客户端进程中

IContentProvider prov=installProvider(context, holder.provider,

holder.info, true);

……

return prov;

}


以上代码中让人比较头疼的是其中新出现的几种数据类型,如IContentProvider、Content-ProviderHolder。先来分析AMS的getContentProvider。

3.AMS的getContentProvider函数分析

getContentProvider的功能主要由getContentProviderImpl函数实现,故此处可直接对它进行分析。

(1)getContentProviderImpl启动目标进程

getContentProviderImpl函数较长,可分段来看,先来分析下面一段代码。

[—>ActivityManagerService.java:getContentProviderImpl]


private final ContentProviderHolder getContentProviderImpl(

IApplicationThread caller, String name){

ContentProviderRecord cpr;

ProviderInfo cpi=null;

synchronized(this){

ProcessRecord r=null;

if(caller!=null){

r=getRecordForAppLocked(caller);

if(r==null)……//如果查询不到调用者信息,则抛出SecurityException

}//if(caller!=null)判断结束

//name参数为调用进程指定的代表目标CP的authority

cpr=mProvidersByName.get(name);

//如果cpr不为空,表明该CP已经在AMS中注册

boolean providerRunning=cpr!=null;

if(providerRunning){

……//如果该CP已经存在,则进行对应处理,相关内容可自行阅读

}

//如果目标CP对应进程还未启动

if(!providerRunning){

try{

//查询PKMS,得到指定的ProviderInfo信息

cpi=AppGlobals.getPackageManager().resolveContentProvider(

name, STOCK_PM_FLAGS|

PackageManager.GET_URI_PERMISSION_PATTERNS);

}……

String msg;

//权限检查,此处不作讨论

if((msg=checkContentProviderPermissionLocked(cpi, r))!=null)

throw new SecurityException(msg);

/*

如果system_server还没启动完毕,并且该CP不运行在system_server中,则此时不允许启动CP。

读者还记得哪个CP运行在system_server进程中吗?答案是SettingsProvider

*/

……

ComponentName comp=new ComponentName(cpi.packageName, cpi.name);

cpr=mProvidersByClass.get(comp);

final boolean firstClass=cpr==null;

//初次启动MediaProvider对应进程时,firstClass一定为true

if(firstClass){

try{

//查询PKMS,得到MediaProvider所在的Application信息

ApplicationInfo ai=

AppGlobals.getPackageManager().getApplicationInfo(

cpi.applicationInfo.packageName, STOCK_PM_FLAGS);

if(ai==null)return null;

//在AMS内部通过ContentProviderRecord来保存CP的信息,类似

//ActivityRecord、BroadcastRecord等

cpr=new ContentProviderRecord(cpi, ai, comp);

}……

}//if(firstClass)判断结束


以上代码的逻辑比较简单,主要是为目标CP(即MediaProvider)创建一个ContentProvider-Record对象。结合第6章的知识,AMS为四大组件都设计了对应的数据结构,如ActivityRecord、BroadcastRecord等。

接着看getContentProviderImpl,其下一步的工作就是启动目标进程,代码如下:

[—>ActivityManagerService.java:getContentProviderImpl]


/*

canRunHere函数用于判断目标CP能否运行在前面定义的变量r所对应的进程(即调用者所在进程)

该函数内部做如下判断:

(info.multiprocess||info.processName.equals(app.processName))

&&(uid==Process.SYSTEM_UID||uid==app.info.uid)

就本例而言,MediaProvider不能运行在客户端进程中

*/

if(r!=null&&cpr.canRunHere(r))return cpr;

final int N=mLaunchingProviders.size();

……//查找目标CP对应的进程是否正处于启动状态

//如果i大于等于N,表明目标进程的信息不在mLaunchingProviders中

if(i>=N){

final long origId=Binder.clearCallingIdentity();

……

//调用startProcessLocked函数创建目标进程

ProcessRecord proc=startProcessLocked(cpi.processName,

cpr.appInfo, false,0,"content provider",

new ComponentName(cpi.applicationInfo.packageName,

cpi.name),false);

if(proc==null)return null;

cpr.launchingApp=proc;

//将该进程信息保存到mLaunchingProviders中

mLaunchingProviders.add(cpr);

}

if(firstClass)mProvidersByClass.put(comp, cpr);

mProvidersByName.put(name, cpr);

/*

下面这个函数将为客户端进程和目标CP进程建立紧密的关系,即当目标CP进程死亡后,

AMS将根据该函数建立的关系找到客户端进程并杀死(kill)它们。在7.2.3节

有对这个函数的相关解释

*/

incProviderCount(r, cpr);

if(cpr.launchingApp==null)return null;

try{

cpr.wait();//等待目前进程的启动

}……

}//synchronized(this)结束

return cpr;

}


通过对以上代码的分析发现,getContentProviderImpl将等待一个事件,想必读者也能明白,此处一定是在等待目标进程启动并创建好MediaProvider。目标进程的这部分工作用专业词语来表达就是发布(publish)目标ContentProvider(即本例的MediaProvider)。

(2)MediaProvider的创建

根据第6章的介绍,目标进程启动后要做的第一件大事就是调用AMS的attachApplication函数,该函数的主要功能由attachApplicationLocked完成。我们回顾一下相关代码。

[—>ActivityManagerService.java:attachApplicationLocked]


private final boolean attachApplicationLocked(IApplicationThread thread,

int pid){

……

//通过PKMS查询运行在该进程中的CP信息,并保存到mProvidersByClass中

List providers=normalMode?

generateApplicationProvidersLocked(app):null;

//调用目标应用进程的bindApplication函数,此处将providers信息传递给目标进程

thread.bindApplication(processName, appInfo, providers,

app.instrumentationClass, profileFile,

……);

……

}


再来看目标进程bindApplication的实现,其内部最终会通过handleBindApplication函数处理,我们回顾一下相关代码。

[—>ActivityThread.java:handleBindApplication]


private void handleBindApplication(AppBindData data){

……

if(!data.restrictedBackupMode){

List<ProviderInfo>providers=data.providers;

if(providers!=null){

//调用installContentProviders安装此ContentProvider

installContentProviders(app, providers);

……

}

}

……

}


AMS传递过来的ProviderInfo列表将由目标进程的installContentProviders处理,其相关代码如下:

[—>ActivityThread.java:installContentProviders]


private void installContentProviders(Context context,

List<ProviderInfo>providers){

final ArrayList<IActivityManager.ContentProviderHolder>results=

new ArrayList<IActivityManager.ContentProviderHolder>();

Iterator<ProviderInfo>i=providers.iterator();

while(i.hasNext()){

//①调用installProvider,注意该函数传递的第二个参数为null

IContentProvider cp=installProvider(context, null, cpi, false);

if(cp!=null){

IActivityManager.ContentProviderHolder cph=

new IActivityManager.ContentProviderHolder(cpi);

cph.provider=cp;

results.add(cph);//将信息添加到results数组中

……//创建引用计数

}

}//while循环结束

try{//②调用AMS的publishContentProviders发布ContentProviders

ActivityManagerNative.getDefault().publishContentProviders(

getApplicationThread(),results);

}……

}


以上代码列出了两个关键点,分别是:

调用installProvider得到一个IContentProvider类型的对象。

调用AMS的publishContentProviders发布本进程所运行的CP。该函数留到后面再作分析。

在继续分析之前,笔者要特别强调installProvider,该函数既在客户端进程中被调用(还记得7.2.2节ActivityThread的acquireProvider函数中那句注释吗?),又在目标进程(即此处MediaProvider所在进程)中被调用。与客户端进程的调用相比,只在一处有明显的不同:

客户端进程调用installProvider函数时,该函数的第二个参数不为null。

目标进程调用installProvider函数时,该函数的第二个参数硬编码为null。

我们曾经在6.2.3分析过installProvider函数,结合那里的介绍可知:installProvider是一个通用函数,不论客户端使用远端的CP还是目标进程安装运行在其上的CP,最终都会调用它,只不过参数不同罢了。

下面来看installProvider函数,其代码如下:

[—>ActivityThread.java:installProvider]


private IContentProvider installProvider(Context context,

IContentProvider provider, ProviderInfo info, boolean noisy){

ContentProvider localProvider=null;

if(provider==null){//针对目标进程的情况

Context c=null;

ApplicationInfo ai=info.applicationInfo;

if(context.getPackageName().equals(ai.packageName)){

c=context;

}……//这部分代码已经在6.2.3节分析过了,其目的就是为了得到正确的

//Context用于加载Java字节码

try{

final java.lang.ClassLoader cl=c.getClassLoader();

//通过Java反射机制创建MediaProvider实例

localProvider=(ContentProvider)cl.

loadClass(info.name).newInstance();

//注意下面这句代码

provider=localProvider.getIContentProvider();

}

}else if(localLOGV){

Slog.v(TAG,"Installing external provider"+info.authority+":"

+info.name);

}//if(provider==null)判断结束

/*

由以上代码可知,对于provider不为null的情况(即客户端调用的情况),该函数没有

什么特殊的处理

*/

……

/*

引用计数及设置DeathReceipient等相关操作。在6.2.3节中曾介绍过,目标进程为自己

进程中的CP实例设置DeathReceipient没有作用,因为二者在同一个进程中,自己怎么能

接收自己的讣告消息呢?不过,如果客户端进程为目标进程的CP设置DeathReceipient又

有作用吗?请读者仔细思考这个问题

*/

return provider;//最终返回的对象是IContentProvider类型的,它到底是什么呢?

}


由以代码可知,installProvider最终返回的是一个IContentProvider类型的对象。对于目标进程而言,该对象是通过调用CP的实例对象的(本例就是MediaProvider)getIContentProvider函数得来的。而对于客户端进程而言,该对象是由installProvider第二个参数传递进来的,那么,这个IContentProvider到底是什么?

(3)IContentProvider的真面目

要说清楚IContentProvider,就要先来看ContentProvider家族的类图,如图7-2所示。

7.2.2 MediaStore.Image.Media的query函数分析 - 图2

图 7-2 ContentProvider类图

图7-2揭示了IContentProvider的真面目,具体介绍如下:

每个CP实例中都有一个mTransport成员,其类型为Transport。

Transport类从ContentProviderNative派生。由图7-2可知,ContentProviderNative从Binder类派生,并实现了IContentProvider接口。结合前面的代码,IContentProvider将是客户端进程和目标进程交互的接口,即目标进程使用IContentProvider的Bn端Transport,而客户端使用IContentProvider的Bp端,其类型是ContentProviderProxy(定义在ContentProviderNative.java中)。

客户端如何通过IContentProvider query函数和目标CP进程进行交互?其流程如下:

CP客户端得到IContentProvider的Bp端(实际类型是ContentProviderProxy),并调用其query函数,在该函数内部将参数信息打包,传递给Transport(它是IContentProvider的Bn端)。

Transport的onTransact函数将调用Transport的query函数,而Transport的query函数又将调用CP子类定义的query函数(即MediaProvider的query函数)。

关于目标进程这一系列的调用函数,不妨先看看Transport的query函数,其代码为:

[—>ContentProvider.java:Transport.query]


public Cursor query(Uri uri, String[]projection,

String selection, String[]selectionArgs, String sortOrder){

enforceReadPermission(uri);

//Transport为ContentProvider的内部类,此处将调用ContentProvider的query函数

//本例中,该query函数由MediaProvider实现,故最终会调用MediaProvider的query

return ContentProvider.this.query(uri, projection, selection,

selectionArgs, sortOrder);

}


读者务必要弄清楚,此处只有一个目标CP的实例,即只有一个MediaProvider对象。Transport的query函数内部虽然调用的是基类CP的query函数,但是根据面向对象的多态原理,该函数最终由其子类(本例中是MediaProvider)来实现。

认识了IContentProvider,即知道了客户端进程和目标进程的交互接口。

继续我们的分析。此时目标进程需要将MediaProvider的信息通过AMS发布出去。

(4)AMS pulishContentProviders分析

要把目标进程的CP信息发布出去,需借助AMS的pulishContentProviders函数,其代码如下:

[—>ActivityManagerService.java:publishContentProviders]


public final void publishContentProviders(IApplicationThread caller,

List<ContentProviderHolder>providers){

……

synchronized(this){

final ProcessRecord r=getRecordForAppLocked(caller);

final long origId=Binder.clearCallingIdentity();

final int N=providers.size();

for(int i=0;i<N;i++){

ContentProviderHolder src=providers.get(i);

ContentProviderRecord dst=r.pubProviders.get(src.info.name);

if(dst!=null){

……//将相关信息分别保存到mProviderByClass和mProvidersByName中

int NL=mLaunchingProviders.size();

……//目标进程已经启动,将其从mLaunchingProviders中移除

synchronized(dst){

dst.provider=src.provider;//将信息保存在dst中

dst.proc=r;

//触发还等在(wait)getContentProvider中的那个客户端进程

dst.notifyAll();

}

updateOomAdjLocked(r);//调节目标进程的oom_adj等相关参数

}//if(dst!=null)判断结束

……

}

}


至此,客户端进程将从getContentProvider中返回,并调用installProvider函数。根据前面的分析,客户端进程调用installProvider时,其第二个参数不为null,即客户端进程已经从AMS中得到了能直接和目标进程交互的IContentProvider Bp端对象。此后,客户端就可

直接使用该对象向目标进程发起请求。