------本文转载自 Android插件化原理解析——contentprovider的插件化 这一系列的文章实在是写的好!
4. 插件化
4.1 思路分析
在分析ContentProvider的工作原理的过程中我们提出了一种插件化方案:在进程启动之初,
手动把ContentProvider安装到本进程,使得后续对于插件ContentProvider的请求能够顺利完成。
我们也指出它的一个严重缺陷,那就是它只能在插件系统内部掩耳盗铃,在插件系统之外,
第三方App依然无法感知到插件中的ContentProvider的存在。
如果插件的ContentProvider组件仅仅是为了共享给其他插件或者宿主程序使用,那么这种方案可以解决问题;
不需要Hook AMS,非常简单。
但是,如果希望把插件ContenProvider共享给整个系统呢?在分析AMS中获取ContentProvider的过程中我们了解到,
ContentProvider信息的注册是在Android系统启动或者新安装App的时候完成的,而AMS把ContentProvider返回
给第三方App也是在system_server进程完成;我们无法对其暗箱操作。
在完成Activity,Service组件的插件化之后,这种限制对我们来说已经是小case了:我们在宿主程序里面注册一个货真价实、
被系统认可的StubContentProvider组件,把这个组件共享给第三方App;
然后通过代理分发技术把第三方App对于插件ContentProvider的请求通过这个StubContentProvider分发给对应的插件。
但是这还存在一个问题,由于第三方App查阅的其实是StubContentProvider,因此他们查阅的URI也必然是
StubContentProvider的authority,要查询到插件的ContentProvider,必须把要查询的真正的插件ContentProvider信息传递进来。
这个问题的解决方案也很容易,我们可以制定一个「插件查询协议」来实现。
举个例子,假设插件系统的宿主程序在AndroidManifest.xml中注册了一个StubContentProvider,
它的Authority为com.test.host_authority;由于这个组件被注册在AndroidManifest.xml中,是系统认可的ContentProvider组件,
整个系统都是可以使用这个共享组件的,使用它的URI一般为content://com.test.host_authority;
那么,如果插件系统中存在一个插件,这个插件提供了一个PluginContentProvider,它的Authority为com.test.plugin_authorith,
因为这个插件的PluginContentProvider没有在宿主程序的AndroidMainifest.xml中注册(预先注册就失去插件的意义了),
整个系统是无法感知到它的存在的;前面提到代理分发技术,也就是,我们让第三方App请求宿主程序的StubContentProvider,
这个 StubContentProvider把请求转发给合适的插件的ContentProvider就能完成了(插件内部通过预先 installProvider可以查询所有的ContentProvider组件);
这个协议可以有很多,比如说:如果第三方App需要请求插件的 StubContentProvider,
可以以content://com.test.host_authority/com.test.plugin_authorith去查询系统;也就是说,
我们假装请求StubContentProvider,把真正的需要请求的PluginContentProvider的 Authority放在路径参数里面,
StubContentProvider收到这个请求之后,拿到这个真正的Authority去请求插件的 PluginContentProvider,
拿到结果之后再返回给第三方App。
这样,我们通过「代理分发技术」以及「插件查询协议」可以完美解决「共享」的问题,
开篇提到了我们之前对于Activity,Service组件插件化方案中对于「共享」功能的缺失,按照这个思路,基本可以解决这一系列问题。
比如,对于第三方App无法绑定插件服务的问题,我们可以注册一个StubService,把真正需要bind的插件服务信息放在intent的某个字段中,
然后在StubService的onBind中解析出这个插件 服务信息,然后去拿到插件Service组件的Binder对象返回给第三方。实现步骤如下.
4.2预先installProvider
要实现预先installProvider,我们首先需要知道,所谓的「预先」到底是在什么时候?
前文我们提到过App进程安装ContentProvider的时机非常之早,在Application类的onCreate回调执行之前已经完成了;这意味着什么?
现在我们对于ContentProvider插件化的实现方式是通过「代理分发技术」,也就是说在请求插件ContentProvider的时候会
先请求宿主程序的StubContentProvider;如果一个第三方App查询插件的ContentProvider,而宿主程序没有启动的 话,
AMS会启动宿主程序并等待宿主程序的StubContentProvider完成安装,一旦安装完成就会把得到的IContentProvider
返回给这个第三方App;第三方App拿到IContentProvider这个Binder对象之后就可能发起CURD操作,
如果这个时候插件ContentProvider还没有启动,那么肯定就会出异常;要记住,“这个时候”可能宿主程序的onCreate还没有执行完毕呢!!
所以,我们基本可以得出结论,预先安装这个所谓的「预先」必须早于Application的onCreate方法,
在Android SDK给我们的回调里面,attachBaseContent这个方法是可以满足要求的,它在Application这个对象被创建之后就会立即调用。
解决了时机问题,那么我们接下来就可以安装ContentProvider了。
安装ContentProvider也就是要调用ActivityThread类的installProvider方法,这个方法需要的参数有点多,
而且它的第二个参数IActivityManager.ContentProviderHolder是一个隐藏类,我们不知道如何构造,
就算通过反射构造由于SDK没有暴露稳定性不易保证,我们看看有什么方法调用了这个installProvider。
installContentProviders这个方法直接调用installProvder看起来可以使用,但是它是一个private的方法,还有public的方法吗?继续往上寻找调用链,发现了installSystemProviders这个方法:
public final void installSystemProviders(List<ProviderInfo> providers) {
if (providers != null) {
installContentProviders(mInitialApplication, providers);
}
}
但是,我们说过ContentProvider的安装必须相当早,必须在Application类的attachBaseContent方法内,
而这个mInitialApplication字段是在onCreate方法调用之后初始化的,所以,如果直接使用这个installSystemProviders势必抛出空指针异常;
因此,我们只有退而求其次,选择通过installContentProviders这个方法完成ContentProvider的安装.
要调用这个方法必须拿到ContentProvider对应的ProviderInfo,这个我们在之前也介绍过,可以通过PackageParser类完成,
当然这个类有一些兼容性问题,我们需要手动处理:
/**
* 解析Apk文件中的 <provider>, 并存储起来
* 主要是调用PackageParser类的generateProviderInfo方法
*
* @param apkFile 插件对应的apk文件
* @throws Exception 解析出错或者反射调用出错, 均会抛出异常
*/
public static List<ProviderInfo> parseProviders(File apkFile) throws Exception {
Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);
Object packageParser = packageParserClass.newInstance();
// 首先调用parsePackage获取到apk对象对应的Package对象
Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, PackageManager.GET_PROVIDERS);
// 读取Package对象里面的services字段
// 接下来要做的就是根据这个List<Provider> 获取到Provider对应的ProviderInfo
Field providersField = packageObj.getClass().getDeclaredField("providers");
List providers = (List) providersField.get(packageObj);
// 调用generateProviderInfo 方法, 把PackageParser.Provider转换成ProviderInfo
Class<?> packageParser$ProviderClass = Class.forName("android.content.pm.PackageParser$Provider");
Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Class<?> userHandler = Class.forName("android.os.UserHandle");
Method getCallingUserIdMethod = userHandler.getDeclaredMethod("getCallingUserId");
int userId = (Integer) getCallingUserIdMethod.invoke(null);
Object defaultUserState = packageUserStateClass.newInstance();
// 需要调用 android.content.pm.PackageParser#generateProviderInfo
Method generateProviderInfo = packageParserClass.getDeclaredMethod("generateProviderInfo",
packageParser$ProviderClass, int.class, packageUserStateClass, int.class);
List<ProviderInfo> ret = new ArrayList<>();
// 解析出intent对应的Provider组件
for (Object service : providers) {
ProviderInfo info = (ProviderInfo) generateProviderInfo.invoke(packageParser, service, 0, defaultUserState, userId);
ret.add(info);
}
return ret;
}
解析出ProviderInfo之后,就可以直接调用installContentProvider了:
/**
* 在进程内部安装provider, 也就是调用 ActivityThread.installContentProviders方法
*
* @param context you know
* @param apkFile
* @throws Exception
*/
public static void installProviders(Context context, File apkFile) throws Exception {
List<ProviderInfo> providerInfos = parseProviders(apkFile);
for (ProviderInfo providerInfo : providerInfos) {
providerInfo.applicationInfo.packageName = context.getPackageName();
}
Log.d("test", providerInfos.toString());
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
Method installProvidersMethod = activityThreadClass.getDeclaredMethod("installContentProviders", Context.class, List.class);
installProvidersMethod.setAccessible(true);
installProvidersMethod.invoke(currentActivityThread, context, providerInfos);
}
整个安装过程必须在Application类的attachBaseContent里面完成:
/**
* 一定需要Application,并且在attachBaseContext里面Hook
* 因为provider的初始化非常早,比Application的onCreate还要早
* 在别的地方hook都晚了。
*/
public class UPFApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
File apkFile = getFileStreamPath("testcontentprovider-debug.apk");
if (!apkFile.exists()) {
Utils.extractAssets(base, "testcontentprovider-debug.apk");
}
File odexFile = getFileStreamPath("test.odex");
// Hook ClassLoader, 让插件中的类能够被成功加载
BaseDexClassLoaderHookHelper.patchClassLoader(getClassLoader(), apkFile, odexFile);
ProviderHelper.installProviders(base, getFileStreamPath("testcontentprovider-debug.apk"));
} catch (Exception e) {
throw new RuntimeException("hook failed", e);
}
}
}
4.3代理分发以及协议解析
把插件中的ContentProvider安装到插件系统中之后,在插件内部就可以自由使用这些ContentProvider了;
要把这些插件共享给整个系统,我们还需要一个货真价实的ContentProvider组件来执行分发:
<provider
android:name="com.example.weishu.contentprovider_management.StubContentProvider"
android:authorities="com.example.weishu.contentprovider_management.StubContentProvider"
android:process=":p"
android:exported="true" />
第三方App如果要查询到插件的ContentProvider,必须遵循一个「插件查询协议」,
这样StubContentProvider才能把对于插件的请求分发到正确的插件组件:
/**
* 为了使得插件的ContentProvder提供给外部使用,我们需要一个StubProvider做中转;
* 如果外部程序需要使用插件系统中插件的ContentProvider,不能直接查询原来的那个uri
* 我们对uri做一些手脚,使得插件系统能识别这个uri;
* 这里的处理方式如下:
* 原始查询插件的URI应该为:content://plugin_auth/path/query
* 如果需要查询插件,需要修改为: content://stub_auth/plugin_auth/path/query
* 也就是,我们把插件ContentProvider的信息放在URI的path中保存起来;
* 然后在StubProvider中做分发。
* 当然,也可以使用QueryParamerter,比如:
* content://plugin_auth/path/query/ -> content://stub_auth/path/query?plugin=plugin_auth
* @param raw 外部查询我们使用的URI
* @return 插件真正的URI
*/
private Uri getRealUri(Uri raw) {
String rawAuth = raw.getAuthority();
if (!AUTHORITY.equals(rawAuth)) {
Log.w(TAG, "rawAuth:" + rawAuth);
}
String uriString = raw.toString();
uriString = uriString.replaceAll(rawAuth + '/', "");
Uri newUri = Uri.parse(uriString);
Log.i(TAG, "realUri:" + newUri);
return newUri;
}
通过以上过程我们就实现了ContentProvider的插件化。需要说明的是,DroidPlugind的插件化与上述介绍的方案有一些不同之处:
1.首先DroidPlugin并没有选择预先安装的方案,而是选择HookActivityManagerNative,
拦截它的getContentProvider以及publishContentProvider方法实现 对于插件组件的控制;
从这里可以看出它对ContentProvider与Service的插件化几乎是相同的,Hook才是DroidPlugin Style ^_^.
2.然后,关于携带插件信息,或者说「插件查询协议」方面;DroidPlugin把插件信息放在查询参数里面,本文呢则是路径参数;这一点完全看个人喜好。
5. 小结
本文我们通过「代理分发技术」以及「插件查询协议」完成了ContentProvider组件的插件化,
并且给出了对「插件共享组件」的问题的一般解决方案。值得一提的是,系统的ContentProvider其实是lazy load的,
也就是说只有在需要使用的时候才会启动对应的ContentProvider,而我们对于插件的实现则是预先加载,
这里还有改进的空间,读者可以思考一下解决方案。
由于ContentProvider的使用频度非常低,而很多它使用的场景(比如系统)并不太需要「插件化」,
因此在实际的插件方案中,提供 ContentProvider插件化的方案非常之少;就算需要实现ContentProvider的插件化,
也只是解决插件内部之间共享组件的问题,并没有把插件组件暴露给整个系统。我个人觉得,如果只是希望插件化,
那么是否支持ContentProvider无伤大雅,但是,如果希望实现虚拟化或者说容器技术,所有组件是必须支持插件化的。