android通过ContentProvider可以实现进程间的数据共享,例如APP通过MediaProvider可以访问多媒体数据库的内容。通常我们在Activity通过getContentResolver().query来跨进程访问数据库,ContentImpl.getContentResolver会返回ContentResolver对象,
![img_04f263b0976707a163f122bb468e1234.png](https://i-blog.csdnimg.cn/blog_migrate/11377f37e3f9daf97760d5e9cda03fca.png)
mContentResolver的具体类型为ApplicationContentResolver,它继承自ContentResolver;在获取到ContentResolver通过其query方法可以跨进程查询数据库,ContentResolver的query方法如下:
![img_d74fa6638f67c4c09c2fea97f59e2b13.png](https://i-blog.csdnimg.cn/blog_migrate/ae23e0d0512bfd1ca7d88a3c53e76633.png)
首先获取了unstableProvider对象,获取经过:ContentResolver.acquireUnstableProvider-->ApplicationContentResolver.acquireUnstableProvider
![img_e4021f677c5b4c7653ab78dc4e8b12a1.png](https://i-blog.csdnimg.cn/blog_migrate/c62f48c2a609eb3bbf3d8dd194eb3da2.png)
ApplicationContentResolver.acquireUnstableProvider调用了mMainThread.acquireProvider来获得IContentProvider,该对象可以远程访问对方进程的ContentProvider,mMainThread就是当前进程的ActivityThread类。
![img_66d2bcbbaa8cc2b8cfe3e215de02b306.png](https://i-blog.csdnimg.cn/blog_migrate/19af1feadf32bf38ecb17f35feb5c67b.png)
acquireExistingProvider函数如下:
![img_eceeb8eab13e99ea76000edf49b46477.png](https://i-blog.csdnimg.cn/blog_migrate/b8325dd0d8fe470b5978d057d3fb0e9a.png)
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](https://i-blog.csdnimg.cn/blog_migrate/7bdda3381614cedf4dbfd438e371aa56.png)
如果ContentProvider对应的进程没有启动,就会调用startProcessLocked启动进程,创建进程就是通过Zygote创建,并运行进程的入口类ActivtyThread.main。startProcessLocked是异步的,所以无法立即确定进程是否启动完成。所以getContentProviderImpl函数中实现了如下代码来等待ContentProvider所属的进程创建并完成ContentProvider的安装:
![img_49bb1ebf9acda45bebe79b63e6d82cd0.png](https://i-blog.csdnimg.cn/blog_migrate/3e149f674b414690cfb1abecc39762fc.png)
那么ContentProvider是什么时机安装的呢,答案就是ActivityThread.handleBindApplication:
![img_0c3de24f9a0456cae46f05ff6725f6ba.png](https://i-blog.csdnimg.cn/blog_migrate/f2c8091e4178b728c867d7c197f33a36.png)
进程创建后会运行Application,这里调用了installContentProviders完成ContentProvider的安装,注意安装ContentProvider发生在Application的onCreate之前。
![img_8b150755f66f1139b1e14d0a0989ce8c.png](https://i-blog.csdnimg.cn/blog_migrate/727681c63b2e2414bd3bb31a0d82aace.png)
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](https://i-blog.csdnimg.cn/blog_migrate/76b68090eebb62594462eb7a6f1ea869.png)
占坑的ContentProvider已经有了,那么接下来该怎么做呢?这里分为两个方面:
1、插件内部访问插件ContentProvider
插件内部使用的Context是PluginContext,VirtualApk复写了getContentResolver函数,返回了自己实现的PluginContentResolver。PluginContentResolver内部重写了acquireProvider、acquireExistingProvider、acquireUnstableProvider函数,这里以acquireUnstableProvider为例:
![img_7f2c8c8449b828144bfae1c1c2c7b495.png](https://i-blog.csdnimg.cn/blog_migrate/13a2f080984a7da0b580f8fef4fba743.png)
mPluginManager.resolveContentProvider判断是否是查询插件的ContentProvider,如果是就使用Hook过对象:
![img_d78cddaa677348c9cbd1e6c624e89af3.png](https://i-blog.csdnimg.cn/blog_migrate/45129e9966bef87af9d81c9508763898.png)
hookIContentProviderAsNeed完成hook工作:
![img_48fa80e6d06ec659e22761a990b67a9b.png](https://i-blog.csdnimg.cn/blog_migrate/0d5b1a47c68c9bac74acc665fb93110e.png)
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](https://i-blog.csdnimg.cn/blog_migrate/290ba7379c7a98bd73085926efc245b9.png)
invoke函数首先将访问插件的Uri转到宿主占坑Uri:
![img_4faecccc264fe547a58e91421416c450.png](https://i-blog.csdnimg.cn/blog_migrate/809c5ee3d11d5d21fc010cedb8d692a8.png)
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](https://i-blog.csdnimg.cn/blog_migrate/c6f097bd40e675a3630f235d211e5ccd.png)
![img_c49411bb800334a1d06d480bd1d52024.png](https://i-blog.csdnimg.cn/blog_migrate/bf0fa70d22672295688d17ad569f4c46.png)
getContentProvider通过反射创建出插件ContentProvider对象,最后调用ContentProvider.query完成查询。