Activity 是App中使用频率最高的组件,各种插件化框架的主要精力都放在Activity上。
Activity的插件化需要解决3方面的技术问题:
1) 宿主App 可以加载插件App中类
2)宿主App 可以加载插件App中的资源
3)宿主App可以加载插件中的Activity
插件化加载插件的Activity的方式有很多中,本文采用的是宿主App合并多个插件dex.
Android 系统对dexpath的处理,在BaseDexClassLoader和DexPathList这两个类中,将拆分dexPath生成的数组,会转换为DexPathList类的dexElements数组。
合并插件Dex:
步骤:
1) 根据宿主的ClassLoader, 获取宿主的dexElements字段
首先反射出BaseDexClassLoader的pathList 字段,它是DexPathList类型。然后反射出DexPathList的dexElements字段,这是个数组
2)根据插件apk, 反射出一个Element 类型的对象,即插件dex
3) 根据插件dex 和宿主dexElements合并成一个新的dex数组,替换宿主之前的dexElements字段。
4)通过Hook AssetManger的addAssetPath 解决插件Activity 资源加载问题
代码如下:
/**
* 合并插件dex 到主dex
*/
public void combinePluginDex(ClassLoader classLoader, File apkFile, File optDexFile){
try{
//获取BaseDexClassLoader的变量pathList
Object pathListObject = ReflectUtil.getFieldObject(DexClassLoader.class.getSuperclass(),classLoader,"pathList");
//获取DexPathList的 Elements[] dexElements
Object[] dexElements = (Object[]) ReflectUtil.getFieldObject(pathListObject,"dexElements");
//创建一个数组,用来替换原始的数组
Class<?> elementClass = dexElements.getClass().getComponentType(); //获取Element类型
Object[] newElements = (Object[]) Array.newInstance(elementClass,dexElements.length+1);
//构造插件Element(File file,boolean isDirectory, File zip, DexFile dexFile)
Class[] paramClass = {File.class, boolean.class, File.class, DexFile.class};
Object[] paramValue = {apkFile,false, apkFile,
DexFile.loadDex(apkFile.getCanonicalPath(),optDexFile.getAbsolutePath(),0)};
Object elementObject = ReflectUtil.createObject(elementClass,paramClass,paramValue);
Object[] toAddElementArray = new Object[]{elementObject};
//将原始的Element 复制进去
System.arraycopy(dexElements,0,newElements,0,dexElements.length);
//将插件Element 复制进去
System.arraycopy(toAddElementArray,0,newElements,dexElements.length,toAddElementArray.length);
//替换
ReflectUtil.setFileObject(pathListObject,"dexElements",newElements);
}catch (Exception e){
e.printStackTrace();
}
}
加载插件中资源:
/**
* 加载资源文件
*/
public void loadResources(Context mContext,String apkPath){
try{
File extractFile = mContext.getFileStreamPath(apkPath);
String path= extractFile.getPath();
assetManager = AssetManager.class.newInstance();
ReflectUtil.invokeInstanceMethod(assetManager,"addAssetPath",new Class[]{String.class},new String[]{path});
mResource = new Resources(assetManager,mContext.getResources().getDisplayMetrics(),mContext.getResources().getConfiguration());
mTheme = mResource.newTheme();
}catch (Exception e){
e.printStackTrace();
}
}
启动插件Activity:
插件Activity 分两种:
1) 插件Activity 在 插件Mainfest.xml中注册的:
这种可以直接启动:
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.xiongliang.plugin1", "com.xiongliang.plugin1.MainActivity"));
startActivity(intent);
2) 插件Activity 没有在插件Mainfest.xml中注册:
这种则需要使用到宿主App中的占位Activity.
首先Hook ActivityManagerNative, 将真正要启动的插件Activity 替换为在宿主Mainfest.xml中声明的替身SubActivity, 进而骗过AMS. 然后 通过对ActivityThread 进行Hook, 将SubActivity 替换为真正要启动的Activity.