VirtualApk源码分析-ContentProvider插件化

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

img_04f263b0976707a163f122bb468e1234.png
ContentImpl.getContentResolver

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

img_d74fa6638f67c4c09c2fea97f59e2b13.png
ContentResolver.query

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

img_e4021f677c5b4c7653ab78dc4e8b12a1.png
ApplicationContentResolver.acquireUnstableProvider

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

img_66d2bcbbaa8cc2b8cfe3e215de02b306.png
ActivityThread.acquireProvider

acquireExistingProvider函数如下:

img_eceeb8eab13e99ea76000edf49b46477.png
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的安装。

img_023ef1bfe568663180050e02ee2bd9a6.png
getProcessRecordLocked

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

img_49bb1ebf9acda45bebe79b63e6d82cd0.png
getContentProviderImpl等待进程完成Provider的安装

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

img_0c3de24f9a0456cae46f05ff6725f6ba.png
ActivityThread.handleBindApplication

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

img_8b150755f66f1139b1e14d0a0989ce8c.png
ActivityThread.installContentProviders

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就是这么搞的。

img_76dd19d8396b4405fc3169853d4c8a8e.png
占坑的ContentProvider

占坑的ContentProvider已经有了,那么接下来该怎么做呢?这里分为两个方面:

1、插件内部访问插件ContentProvider

插件内部使用的Context是PluginContext,VirtualApk复写了getContentResolver函数,返回了自己实现的PluginContentResolver。PluginContentResolver内部重写了acquireProvider、acquireExistingProvider、acquireUnstableProvider函数,这里以acquireUnstableProvider为例:

img_7f2c8c8449b828144bfae1c1c2c7b495.png
acquireUnstableProvider

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

img_d78cddaa677348c9cbd1e6c624e89af3.png
hook的IContentProvider对象

hookIContentProviderAsNeed完成hook工作:

img_48fa80e6d06ec659e22761a990b67a9b.png
hookIContentProviderAsNeeded

hookIContentProviderAsNeeded替换了ActivityThread中已经安装好的auth对应的IContentProvider,改用自己的IContentProviderProxy。

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

img_87222f665f0a06156850798cfbf277cb.png
IContentProviderProxy.invoke

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

img_4faecccc264fe547a58e91421416c450.png
wrapperUri

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,下面就需要进行代理转发:

img_0d1edadb3f38fdf01fdeef42dd63a769.png
RemoteContentProvider.query
img_c49411bb800334a1d06d480bd1d52024.png
RemoteContentProvider.getContentProvider

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值