文章参考:
主流热修复方案
腾讯:Tinder
饿了么:Amigo
阿里:AndFix(已停止维护)
上面两个是类加载方案,即Dex插桩;阿里是底层替换方案
Dex插桩方案
原理:类加载
当我们需要热修复的时候,最常见的情况是想短时间内更改应用内的几个.java文件,但是打包apk下载安装流程太长,因此就需要一个更好的解决方案。而为了更好的解决方案,我们需要先了解我们的代码最终到了哪。
众所周知,java代码一般会编译成.class文件运行在JVM虚拟机上,但是Android在4.4之前用DVM虚拟机,DVM虚拟机不能直接运行java字节码,只能运行.dex文件,4.4之后ART虚拟机也是运行的.dex文件。对于Android最终安装的apk,也是把dex文件、资源文件和AndroidManifest文件一起打包在一起再进行签名。所以,我们如果需要更新apk,主要就是想更新里头的.dex文件。
而了解代码到了哪后,我们还需要了解代码怎么加载的。
首先在java中,classLoader通过双亲委派模型实现了类加载,ClassLoader在loadClass中,先判断自己又没有加载过这个class文件,没有的话让parent尝试loadClass,如果parent没有再通过findClass进行加载。
不同的层次ClassLoader负责加载不同范围的类,从最基本的Java类到应用程序类。
这样的层次结构允许类的隔离、版本管理和自定义加载,保护了类之间的互相影响
- 对于基本的Java类,android用的BootClassLoader,JVM用的BootstrapClassLoader;
- 对于指定目录的.class文件,android用的DexClassLoader,JVM用的CustomClassLoader;
- 对于系统中已经安装的应用也就是/data/app下的.class文件,android用的PathClassLoader,类似于Java的AppClassLoader。
- 除此之外还有个BaseDexClassLoader,是DexClassLoader和PathClassLoader的父类
其中,DexClassLoader和PathClassLoader都没有实现findClass方法,只有父类BaseDexClassLoader里头有实现。
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 实质是通过pathList的对象findClass()方法来获取class
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
通过源码我们能够得知,findClass最后是通过DexPathList实现
public Class findClass(String name, List<Throwable> suppressed) {
// 遍历从dexPath查询到的dex和资源Element
for (Element element : dexElements) {
DexFile dex = element.dexFile;
// 如果当前的Element是dex文件元素
if (dex != null) {
// 使用DexFile.loadClassBinaryName加载类
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
到此,我们知道class文件是遍历dexElements找到的,而这个dexElements是在DexPathList的构造方法中,通过makeDexElements方法初始化的。
因此,如果我们通过反射,拿到DexPathList的dexElements,然后把含有要替换的.class的dex包装成Element,放到dexElements这个数组的前边,那么通过遍历加载.class的时候就能优先加载我们要替换的.class的文件了。
p.s. 当然,由于ClassLoader对于已经加载的类存在缓存,因此当类被加载过后,不会从新添加的dex文件中进行寻找,只能通过重启生效。
那么最后的问题,如何把.java文件包装为dex文件
很简单,就两步
- 通过 javac 命令将.java编译成.class文件
- 通过Android的buildTools中的dx.bat将.class编译成.dex,这个bat文件一般路径为
Android\sdk\build-tools\30.0.3\dx.bat
最后放一个demo进行演示
//在Application中进行替换
public class MApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//dex作为插件进行加载
dexPlugin();
}
...
/**
* dex作为插件加载
*/
private void dexPlugin(){
//插件包文件
File file = new File("/sdcard/FixDexTest.dex");
if (!file.exists()) {
Log.i("MApplication", "插件包不在");
return;
}
try {
//获取到 BaseDexClassLoader 的 pathList字段
// private final DexPathList pathList;
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
//破坏封装,设置为可以调用
pathListField.setAccessible(true);
//拿到当前ClassLoader的pathList对象
Object pathListObj = pathListField.get(getClassLoader());
//获取当前ClassLoader的pathList对象的字节码文件(DexPathList )
Class<?> dexPathListClass = pathListObj.getClass();
//拿到DexPathList 的 dexElements字段
// private final Element[] dexElements;
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
//破坏封装,设置为可以调用
dexElementsField.setAccessible(true);
//使用插件创建 ClassLoader
DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
//拿到插件的DexClassLoader 的 pathList对象
Object newPathListObj = pathListField.get(pathClassLoader);
//拿到插件的pathList对象的 dexElements变量
Object newDexElementsObj = dexElementsField.get(newPathListObj);
//拿到当前的pathList对象的 dexElements变量
Object dexElementsObj=dexElementsField.get(pathListObj);
int oldLength = Array.getLength(dexElementsObj);
int newLength = Array.getLength(newDexElementsObj);
//创建一个dexElements对象
Object concatDexElementsObject = Array.newInstance(dexElementsObj.getClass().getComponentType(), oldLength + newLength);
//先添加新的dex添加到dexElement
for (int i = 0; i < newLength; i++) {
Array.set(concatDexElementsObject, i, Array.get(newDexElementsObj, i));
}
//再添加之前的dex添加到dexElement
for (int i = 0; i < oldLength; i++) {
Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObj, i));
}
//将组建出来的对象设置给 当前ClassLoader的pathList对象
dexElementsField.set(pathListObj, concatDexElementsObject);
} catch (Exception e) {
e.printStackTrace();
}
}
最后对完整的热修复方案进行简单设计
- 通过定时任务请求后台,后台通过版本确认是否需要热修复,需要热修复的把对应.dex文件下载到指定文件夹(ex:{path}/hotfix/{app_version})并更新DexPathList的dexElements,如果没有加载过可以直接生效
- App每次启动,把指定文件夹的.dex文件更新到DexPathList的dexElements中
- 对于紧急的热修复,可以增加弹窗提醒用户重启应用。
注意,用于热修复的.dex文件应该指定版本,不然新的apk中的代码无法被加载。

1031

被折叠的 条评论
为什么被折叠?



