Dex更新
- 动态改变BaseDexClassLoader对象间接引用的dexElements:
ClassLoader加载流程:从DexElements依次遍历dex,如果dex中有该类则返回,没有则遍历下一个dex,所以Hotfix的解决方式就是改变dexElements中dex的顺序
1. 通过反射获取应用的pathClassloader—>PathList—>DexElements
2. 接着获取补丁dex的dexClassloader—>PathList—>DexElements
3. 然后通过combinArray的方法将2个DexElements合并,补丁的DexElements放在前面
4. 然后使用合并后的DexElements作为PathdexClassloader中的DexElements
5. 这样在加载的时候就可以优先加载到补丁dex,从中可以加载到我们的补丁类。
- 在app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志:
相关类之所以会被打上CLASS_ISPREVERIFIED,
是因为类和引用它的类在同一个dex文件中,那么引用它的类就会被CLASS_ISPREVERIFIED标志,
这样在采用分包方案时,假如类和引用它的类不在一个dex文件中,程序就会报错。
解决方案:
在每个类的构造方法中通过javassist注入代码加载辅助类,
辅助类是在一个单独的类,单独会被打包成一个dex文件。
另外为了实现代码在执行dex命令前注入,需要在build.gradle中新建一个task,使其在执行dex前执行。
代码示例:
import android.content.Context;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;
/**
* @author: Coding.He
* @date: 2020/4/14
* @des:from https://blog.csdn.net/zly921112/article/details/83547750
* 注意:/storage/emulated/0/Android/data/app包名/files/patch
* <p>
*/
public class DexFixImpl {
private static final String TAG = "DexFixImpl";
private static final String FIELD_DEX_ELEMENTS = "dexElements";
private static final String CLASS_NAME = "dalvik.system.BaseDexClassLoader";
private static final String FIELD_PATH_LIST = "pathList";
/**
* 默认 dex优化存放目录
* /data/data/app包名/app_odex
*/
private static final String OPTIMIZE_DIR = "dex2opt";
/**
* 获取应用程序相关的缓存路径都不需要权限
*/
public static void startFix(Context context, String apkPath) throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
if (context == null) {
Log.i(TAG, "context is null");
return;
}
Context appCtx = context.getApplicationContext();
File optFile = context.getDir(OPTIMIZE_DIR, Context.MODE_PRIVATE);
Log.d(TAG, "optFile:" + optFile.getAbsolutePath());
if (!optFile.exists()) {
optFile.mkdir();
}
//遍历查找文件中patch开头, .dex .jar .apk结尾的文件
String optPath = optFile.getAbsolutePath();
//拿到系统默认的PathClassLoader加载器
ClassLoader pathClassLoader = appCtx.getClassLoader();
//加载我们自己的补丁dex
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, optPath, null, appCtx.getClassLoader());
//获取PathClassLoader Element[]
Object pathElements = getElements(pathClassLoader);
//获取DexClassLoader Element[]
Object dexElements = getElements(dexClassLoader);
//合并数组
Object combineArray = combineArray(pathElements, dexElements);
//将合并后Element[]数组设置回PathClassLoader pathList变量
setDexElements(pathClassLoader, combineArray);
}
/**
* 获取Element[]数组
*/
private static Object getElements(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//拿到BaseDexClassLoader Class
Class<?> BaseDexClassLoaderClazz = Class.forName(CLASS_NAME);
//拿到pathList字段
Field pathListField = BaseDexClassLoaderClazz.getDeclaredField(FIELD_PATH_LIST);
Log.d(TAG, "pathListField:" + pathListField);
pathListField.setAccessible(true);
//拿到DexPathList对象
Object DexPathList = pathListField.get(classLoader);
//拿到dexElements字段
Field dexElementsField = DexPathList.getClass().getDeclaredField(FIELD_DEX_ELEMENTS);
dexElementsField.setAccessible(true);
//拿到Element[]数组
return dexElementsField.get(DexPathList);
}
/**
* 合并Element[]数组 将补丁的放在前面
*/
private static Object combineArray(Object pathElements, Object dexElements) {
Class<?> componentType = pathElements.getClass().getComponentType();
int i = Array.getLength(pathElements);
int j = Array.getLength(dexElements);
int k = i + j;
// 创建一个类型为componentType,长度为k的新数组
Object result = Array.newInstance(componentType, k);
System.arraycopy(dexElements, 0, result, 0, j);
System.arraycopy(pathElements, 0, result, j, i);
return result;
}
/**
* 将Element[]数组 设置回PathClassLoader
*/
private static void setDexElements(ClassLoader classLoader, Object value) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Class<?> BaseDexClassLoaderClazz = Class.forName(CLASS_NAME);
Field pathListField = BaseDexClassLoaderClazz.getDeclaredField(FIELD_PATH_LIST);
pathListField.setAccessible(true);
Object dexPathList = pathListField.get(classLoader);
Field dexElementsField = dexPathList.getClass().getDeclaredField(FIELD_DEX_ELEMENTS);
dexElementsField.setAccessible(true);
dexElementsField.set(dexPathList, value);
}
}
Res更新
方案1:
1. 创建一个新的AssetManager,并通过反射调用addAssetPath添加补丁包
2. 反射得到Activity中的AssetManager的引用处,全部换成刚才新构建的newAssetManager
3. 得到Resources的弱引用集合,把他们的AssetManager成员全部替换为newAssetManager
方案2:
1. 创建一个新的AssetManager,并通过反射调用addAssetPath添加补丁包
2. 使用newAssetManager生成一个newResource对象
3. 在插件apk中使用资源时,用newAssetManager和newResource去加载资源
Tips
1.之所以需要新建一个AssetManager,是因为原Apk中的resource与插件apk中的resource,资源id会重复,从而导致插件apk的资源即使通过addAssetPath方法添加到AssetManager中,但是无法生效。
代码示例:
val pluginAssets = AssetManager::class.java.newInstance()
val addAssetPath: Method = AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
addAssetPath.invoke(pluginAssets, apkPath)
val superResources: Resources? = ctx.resources
val pluginRes = Resources(pluginAssets, superResources?.displayMetrics, superResources?.configuration)