在多年的迭代和升级工作中,组件化项目越来越庞大(几十个模块,近10个第三方播放SDK),直接导致发版困难、方法数超标、工作效率大大降低,质量问题频发等等。项目迫切需要一套方案来解决这些问题。由于我们是自行研发的系统和主板,如果直接使用第三方框架,可能会引起相关的适配问题而不好解决,所以需要实现一套自己的插件化框架,也便于后期进行更多的定制。于是进行了下面粗浅的研究。
项目是影视类项目,引进了很多第三方播放SDK,实际上用户在单次启动时并不会用到这么多的SDK,所以直接加载全部的SDK是很浪费性能的。同时,各个公司的SDK是相互不干扰的,按照以往统一升级的做法,在遇到一家有任何问题的时候只能整体应用升级,非常不方便。且在日常版本迭代中任何一个模块出问题都会直接导致项目延期。。。综合以上情况我们需要做到以下几点:
1、插件的动态加载,使用到再加载,且插件不能安装,不能修改第三方SDK的内容;
2、无差别加载插件,实现在不修改主工程的情况下直接接入新的SDK;
3、插件版本控制和独立升级
在进行插件化研究的过程中发现,直接简单粗暴的合并插件宿主的dex,因为插件的资源文件并没有被加载到当前虚拟机,所以并不能正常使用插件的资源文件。于是进行了下面粗浅的研究。
一:首先我们设想,是不是跟dex的处理办法一样,直接将插件的资源注入到宿主中就可以正常使用了?
答案肯定是不行的,为什么?这里我们首先要了解下Android使用和打包资源的逻辑,Android在打包资源的时候会产生自己的 resources.arsc,并生成R.java文件,大家可以看一下宿主apk的R.java文件和插件apk的R.java文件,会发现两个apk的资源id都是0x7F开头的,0x7F地址头其实是资源打包工具aapt内写死的,所以不同的apk里面的资源id必定是会存在冲突的,这个时候我们直接合并冲突的资源文件,肯定是不行的,因此我们首先要解决资源冲突的问题,怎么解决呢?这里我们使用比较大众的方案,修改资源打包工具aapt,将插件的资源整体迁移到一个新的地址段,比如0x6F,这样将宿主和插件的资源id完全隔离开来,怎么修改aapt文件网上有很多大神解释过了,大家可以自行百度,这里我不在啰嗦,直接给出打包完aapt之后的使用方法,aapt文件可以从我分享的内容里下载。
aapt下载地址:
https://download.csdn.net/download/qq_22117359/11180515
使用方法:
1、将aapt文件放到Android studio使用的sdk路径下面build-tools/你使用的版本号/aapt,替换掉原来的aapt文件。
2、在app工程的build.gradle里面添加:
android {
compileSdkVersion gradle.ext.api
buildToolsVersion gradle.ext.buildTools
//添加如下配置,指定插件的资源id其实地址,不要使用0x01 0x00
aaptOptions {
aaptOptions.additionalParameters '--PLUG-resoure-id', '0x61'
}
}
3、编译apk,编译完成之后查看生成的R.java文件,如果id是以你上面配置定义的地址开始的,证明资源已经按你的地址打包成功了。
二:以上已经完成了资源id的分离,由于没有资源的冲突问题,接下来我们只要把插件apk的资源注入到宿主的资源路径就可以正常使用了。
注入的方法很简单,研究源码可以发现,资源加载到虚拟机最终都是通过AssetManager的addAssetPath方法来添加的,那就简单了,我们先拿到宿主的AssetManager对象,然后hook该对象的addAssetPath方法,通过addAssetPath方法将插件内的资源路径添加进来,就可以让插件内的资源被正常调用了。具体实现如下:
/**
* 注入插件资源到当前虚拟机
* 资源合并(一定要保证插件的资源id和宿主不冲突(修改插件aaptOptions,并修改aapt执行文件))
* @param context 宿主上下文
* @param apkPaths 插件apk路径
* @return
*/
public static boolean injectResource(Context context, String apkPaths)
{
Log.i(TAG, "start inject resource from : "+apkPaths);
AssetManager assetManager = context.getAssets();
try
{
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
int ret = (Integer) addAssetPath.invoke(assetManager, apkPaths);
Log.d(TAG, "inject resource success path = "+ apkPaths + ", ret=" + ret);
return true;
} catch (IllegalAccessException e)
{
e.printStackTrace();
} catch (NoSuchMethodException e)
{
e.printStackTrace();
} catch (IllegalArgumentException e)
{
e.printStackTrace();
} catch (InvocationTargetException e)
{
e.printStackTrace();
}
Log.i(TAG, "inject resource failed : "+apkPaths);
return false;
}
通过以上方法完成了资源的注入,到此我们已经解决了插件化的资源冲突与使用问题。