VirtualApk源码分析-ContentProvider插件
android通过ContentProvider可以实现进程间的数据共享,例如APP通过MediaProvider可以访问多媒体数据库的内容。通常我们在Activity通过getContentResolver().query来跨进程访问数据库,ContentImpl.getContentResolver会返回ContentResolver对象,

mContentResolver的具体类型为ApplicationContentResolver,它继承自ContentResolver;在获取到ContentResolver通过其query方法可以跨进程查询数据库,ContentResolver的query方法如下:

首先获取了unstableProvider对象,获取经过:ContentResolver.acquireUnstableProvider-->ApplicationContentResolver.acquireUnstableProvider

ApplicationContentResolver.acquireUnstableProvider调用了mMainThread.acquireProvider来获得IContentProvider,该对象可以远程访问对方进程的ContentProvider,mMainThread就是当前进程的ActivityThread类。

acquireExistingProvider函数如下:

acquireExistingProvider查询当前进程是否已经保存过该ContentProvider的副本,副本的存在可以提高多次访问同一个ContentProvider的性能,如果不存在副本,ActivityThread.acquireProvider就会调用AMS.getContentProvider函数来进行查找。查找过程如下:AMS.getContentProvider-->AMS.getContentProviderImpl。
getContentProviderImpl函数很长,大概的流程如下:
1、mProviderMap.getProviderByName(name, userId)查找当前是否已经plublish,如果没有,进入第2步
2、 调用AppGlobals.getPackageManager().resolveContentProvider()获取ProviderInfo,其实就是调用PMS.resolveContentProvider,在APK安装的时候,PMS会解析AndroidManifest.xml文件。并将APK的ContentProvider信息保存成ProviderInfo。获取大屏ProviderInfo后进入第3步,
3、创建ContentProviderRecord对象,通过getProcessRecordLocked获取ContentProviderRecord对应的进程是否启动,如果进程已经启动,就将ContentProviderRecord保存到ContentProvider所属进程的pubProviders中,并调用AppliationThread.scheduleInstallProvider(会调用ActivityThread.installContentProviders)进行ContentProvider的安装。

如果ContentProvider对应的进程没有启动,就会调用startProcessLocked启动进程,创建进程就是通过Zygote创建,并运行进程的入口类ActivtyThread.main。startProcessLocked是异步的,所以无法立即确定进程是否启动完成。所以getContentProviderImpl函数中实现了如下代码来等待ContentProvider所属的进程创建并完成ContentProvider的安装:

那么ContentProvider是什么时机安装的呢,答案就是ActivityThread.handleBindApplication:

进程创建后会运行Application,这里调用了installContentProviders完成ContentProvider的安装,注意安装ContentProvider发生在Application的onCreate之前。

ContentProvider安装完成后调用AMS.publishContentProvider,该操作将当前进程的ContentProvider保存到了AMS,这样有其余进程想访问这个ContentProvider的时候,AMS就可以直接返回,不需要再次安装了。
所以说,一个ContentProvider在安装完成后会有两个副本,AMS中一个副本(不同进程访问同一个ContentProvider时可以复用AMS中的对象),访问这个ContentProvider的APP进程中有一个副本(同一个进程的多次访问同一个ContentProvider可以重复使用这个副本)
在了解了ContentProvider的运行流程后,如何对其进行插件化呢?这里有如下思路:
1、能否自己解析插件的内部的ContentProvider信息,然后安装到自己进程内部呢?即安装到ActivityThread的mProviderMap中。
这样做只能保证当前进程能够访问这个ContentProvider,其他进程则无法访问,要解决ContentProvider的共享必须有经过AMS的getContentProvider过程,而且要保证ProviderInfo在PMS已经存在,这显然是不可能的。
2、代理转发,在宿主APP中拦截ContentProvider的操作,然后将操作转给宿主占坑的ContentProvider,然后再由占坑ContentProvider进行转发,调用插件中具体的ContentProvider对象。没错,VirtualApk就是这么搞的。

占坑的ContentProvider已经有了,那么接下来该怎么做呢?这里分为两个方面:
1、插件内部访问插件ContentProvider
插件内部使用的Context是PluginContext,VirtualApk复写了getContentResolver函数,返回了自己实现的PluginContentResolver。PluginContentResolver内部重写了acquireProvider、acquireExistingProvider、acquireUnstableProvider函数,这里以acquireUnstableProvider为例:

mPluginManager.resolveContentProvider判断是否是查询插件的ContentProvider,如果是就使用Hook过对象:

hookIContentProviderAsNeed完成hook工作:

hookIContentProviderAsNeeded替换了ActivityThread中已经安装好的auth对应的IContentProvider,改用自己的IContentProviderProxy。
mContext.getContentResolver().call(uri,"wakeup",null,null);十分重要,uri="content://packageName.VirtualAPK.Provider",他完成了RemoteContentProvider的安装,这样在ActivityThread中就保存了RemoteContentProvider对应的IContentProvider对象,然后给这个对象设置代理即可。这样当当前进程再访问RemoteContentProvider时就直接使用这个代理。

invoke函数首先将访问插件的Uri转到宿主占坑Uri:

Uri的个数变成了content://host_authority/plugin_authority,其中host_authority表示宿主占坑ContentProvider对应的Auth,plugin_authority代表了实际要启动的插件ContentProvider的一些信息。
这样就插件内部在获取插件ContentProvider时的请求就转到了宿主ContentProvider中。
2、插件外部访问插件ContentProvider
插件外部访问插件ContentProvider,首先需要调用PluginContentResolver.wrapperUri将对插件访问的URI转为content://host_authority/plugin_authority格式,然后再由占坑ContentProvider进行转发。
在完成URI转换后,所有请求都转给了宿主占坑ContentProvider,下面就需要进行代理转发:


getContentProvider通过反射创建出插件ContentProvider对象,最后调用ContentProvider.query完成查询。