引言
与插件中静态广播接收器类似,由于插件中的组件没有在Manifest中注册,如果没有宿主对广播进行注册和转发的话,那么就无法起到静态广播的作用(比如拉起进程),ContentProvider也是一样,也需要宿主对ContentProvider的CRUD操作进行转发。
1.插件中代码替换
插件中会涉及到对于ContentProvider的操作有两种,第一种是ContentResolver相关的方法调用; 第二种是ContentProviderClient相关的方法调用。
所以在replugin-plugin-gradle中也有两个负责代码插入的类,第一个是ProviderInjector; 第二个是ProviderInjector2.
1.1 ProviderInjector
首先看一下ProviderInjector的代码:
|
|
includeMethodCall中包含了所有要替换的地方,injectClass()方法中,ctCls.getDeclaredMethods().each{}和ctCls.getMethods().each{}的作用是遍历全部方法,并且执行instrument方法,逐个扫描方法体内的每一行代码,然后交给ProviderExprEditor的edit()处理对方法体代码的修改。
而ProviderExprEditor代码如下:
|
|
方法很简单,就是遇到使用了ContentResolver中在includeMethodCall中的方法调用,就替换成PluginProviderClient的调用。关于PluginProviderClient的分析在下一节。
1.2 ProviderInjector2
ProviderInjector2的实现其实跟ProviderInjector很类似,只不过它要处理的是ContentProviderClient的调用,而ContentProviderClient的调用只有两个方法: query()和update().
下面是ProviderInjector2的实现:
|
|
类似地,ctCls.getDeclaredMethods().each{}和ctCls.getMethods().each{}的作用是遍历全部方法,并执行instrument方法,逐个扫描每个方法体内每一行代码,并交给ProviderExprEditor2的edit()方法处理对方法体代码的修改。
而ProviderExprEditor2的实现如下:
|
|
显然,这个类的作用就是将ContentProviderClient的方法调用替换为PluginProviderClient2的同名方法调用,同时再次吐槽一下这个命名规范。关于PluginProviderClient2的分析见下一节。
2. PluginProviderClient和PluginProviderClient2
上一节中讲到插件中所有ContentResolver的调用都会替换为PluginProviderClient的同名方法调用,而所有ContentProviderClient的调用都会替换为PluginProviderClient2的同名方法调用。
不过,首先需要注意的是,上面说到的PluginProviderClient和PluginProviderClient2都是replugin-plugin-library中的类,而不是replugin-host-library中的类。而这两个类都是采用反射的方式调用replugin-host-library中PluginProviderClient和PluginProviderClient2中的代码。
关于它们是如何包装r反射的就不说了,感兴趣的童鞋自己去看吧,下面要讲的都是replugin-host-library中的PluginProviderClient和PluginProviderClient2.
2.1 PluginProviderClient
PluginProviderClient实现了很多方法,我们以query()为例:
|
|
其中的重点是toCalledUri()这个方法,这个方法在CRUD中都要调用,它的实现如下:
|
|
这里只是通过context获取了plugin的名称,然后调用了另外一个toCalledUri()方法。
继续看toCallUri(Context,String,Uri,int)方法:
|
|
这个方法其实就做了两件时,第一件事是从plugin名称称uri的authority获取到其process, 然后根据process获取坑位ContentProvider的authority,最后进行封装成可以调用坑位ContentProvider的uri.
显然,总结起来,这个方法的作用就是把插件的uri改造成hostpkg+pluguri的方式。
那么经过改造的uri被谁接收了呢?
实际上是坑位ContentProvider, 每个插件中的ContentProvider,即使是有自定义进程的,也会有一个转换关系,让它对应到某个坑位ContentProvider.
那么我们怎么知道是调用坑位ContentProvider的呢?
其实关键就是看authority的赋值,看如下代码片段:
|
|
注意其中的PluginProcessHost.PROCESS_AUTHORITY_MAP是怎么来的:
|
|
注意这里的赋值就用到了PluginPitProviderBase.AUTHORITY_PREFIX, 而PluginPitProviderBase就是所有坑位ContentProvider的基类。
题外话:再次吐槽一下RePlugin糟糕的命名,这里如果改成encodeUri()会好很多,之后对于uri进行还原时可以对应地命名为decodeUri()方法。
2.2 PluginProviderClient2
相比之下,PluignProviderClient2的代码要简单一些,因为它只需要替换插件中ContentProviderClient的query()和update()方法,以query()为例:
|
|
显然,核心也是toCalledUri()这个方法的调用,在上一节分析过,这里就不再啰嗦了。
3.坑位ContentProvider: PluginPitProviderBase
PluginPitProviderBase作为各坑位ContentProvider的基类,实现了CRUD在内的各种方法,仍然以query()为例:
|
|
首先看PluginProviderHelper中的toPluginUri()方法:
|
|
这个方法就是字符串进行处理,将之前包装过的uri还原为插件原本的uri,之后将插件名称和原本的uri保存在PluginUri对象中。
再回到PluginPitProviderBase中的query()方法,获取到PluginUri之后,调用PluginProviderHelper.getProvider()方法找到对应的ContentProvider:
|
|
这个方法很简单,首先根据 authority从缓存中找,如果找到则直接返回;如果没找到则调用installProvider()方法:
|
|
如果看过Android Framework代码的话,对这个方法不会陌生,它们其实非常像,唯一的不同就是这里是从ComponentList中获取ProviderInfo.
获取到ProviderInfo之后,调用插件中的PluignDexClassLoader加载相应的类并生成实例,之后调用相应的回调方法即可。