7.2.2 MediaStore.Image.Media的query函数分析
第二个关键点是在MediaProvider客户端示例中所调用的MediaStore.Image.Media的query函数。MediaStore是多媒体开发中常用的类,其
内部定义了专门针对Image、Audio、Video等不同多媒体信息的内部类来帮助客户端开发人员更好地和MediaProvider交互。这些类及相互之间的关系如图7-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 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端对象。此后,客户端就可
直接使用该对象向目标进程发起请求。