对于热更新的问题就是了解两个点的问题:
- 如何加载补丁包,也就是如何加载dex 文件的过程(dex是补丁包,更改的文件都在补丁包中)
- 修复后的类如何替换掉旧的类
通过这篇文章给大家介绍下我理解的热更新的逻辑,需要先了解一些关系到的知识
热更新方案有三种
- 底层替换方案
- 类加载方案
- Instant Run
本篇文章主要是 类加载 和 Instant Run 两种方式进行的热更新
类加载方案
需要先了解Android 类加载,可以看这篇 https://blog.youkuaiyun.com/hjiangshujing/article/details/104249956
此处用到的是Android 中的 DexClassLoader 类加载器
以下做简单的介绍
Android 类加载
- BootClassLoader
- DexClassLoader – optimizedDirect
- PathClassLoader – 没有 optimizedDirect,默认optimizedDirect 的值为/data/dalvik-cache ,PathClassLoader 无法定义解压的dex文件存储路径,因此它通常用来加载已经安装的apk的dex文件(安装的apk的dex文件会存储在/data/dalvik-cache中)
类加载方案原理
首先要了解类加载过程
class 加载过程从ClassLoader 的loadClass方法开始
ClassLoader 的加载方法为loadClass
可以通过 Android 中的类加载 文章中的最后的图来说
DexPathList.java 中的findClass 方法(核心环节)
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {//1
Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
- 遍历dexElements
- 调用Element 的findClass 方法
- 每个Element 中内部封装了DexFile ,用于加载dex,如果DexFile 不为null,就调用DexFile 的loadClassBinaryName 方法
- loadClassBinaryName 方法中调用了defineClass,调用了defineClassNative方法来加载dex相关文件
那么类加载方案的原理就出来,如下:
- ClassLoader 的加载过程,其中一个环节就是调用DexPathList的findClass 方法
- Element 内部封装了DexFile ,DexFile用于加载dex文件,因此每个dex文件对应一个Element ,多个Element 组成了有序的Element 数组dexElements。
- 当要查找类时,会遍历dexElements (相当于遍历dex 文件数组),并调用DexFile的loadClassBinaryName 方法查找类
- 如果在Element 中(dex 文件)找到了该类就返回
- 如果没有找到就接着在下一个Elment中进行查找
- 将Bug类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar
- 通过反射修改类加载中的dexElements,将补丁包放在Elment 数组dexElements 的第一个元素
- 在类加载的时候先加载到的是Patch.dex中的修复后的Key.class(根据类加载的双亲委托机制,如果此类加载过就不会再加载),这样就做到替换之前存在Bug的Key.class
代码实现
/**
* 修复方法
*/
private static void dexFix(ClassLoader classLoader, File optimizedDirectory, File... dexFiles) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessError, IllegalAccessException {
StringBuilder sb = new StringBuilder();
for (File file : dexFiles) {
sb.append(file.getAbsolutePath()).append(":");
}
//1.使用DexClassLoader 加载所有外部dex文件
DexClassLoader dexClassLoader = new DexClassLoader(sb.deleteCharAt(sb.length() - 1).toString(),
optimizedDirectory.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());
//2.获取系统 dexElements
Object pathElements = getClassLoaderElements(classLoader);
//3.获取外部dex 的dexElements
Object dexElements = getClassLoaderElements(dexClassLoader);
Field pathListField = ReflectUtil.findFiled(classLoader.getClass(), "pathList");
Object pathList = pathListField.get(classLoader);
Field dexElementsFiled = ReflectUtil.findFiled(pathList.getClass(), "dexElements");
//4.将系统与外部dexElements合并
Object arrayAppend = arrayAppend(dexElements, pathElements);
//5.修改系统 dexElements
dexElementsFiled.set(pathList, arrayAppend);
}
/**
* 将所有Array类型的数据按顺序合并成一个Array数据
*/
public static Object arrayAppend(Object... elements) {
int length = 0;
for (Object element : elements) {
length += Array.getLength(element);
}
Object array = Array.newInstance(elements[0].getClass().getComponentType(), length);
for (int i = 0, j = 0, k = 0, elementLength = Array.getLength(elements[k]); i < length; i++) {
Array.set(array, i, Array.get(elements[k], i - j));
if (i - j == elementLength - 1) {
j += elementLength;
k++;
if (k < elements.length) {
elementLength = Array.getLength(elements[k]);
}
}
}
return array;
}
/**
* 获取ClassLoader 中 dexElements 成员变量
*/
private static Object getClassLoaderElements(ClassLoader classLoader) throws NoSuchMethodException, IllegalAccessException {
Field pathListField = ReflectUtil.findFiled(classLoader.getClass(), "pathList");
Object pathList = pathListField.get(classLoader);
Field dexElementsFiled = ReflectUtil.findFiled(pathList.getClass(), "dexElements");
return dexElementsFiled.get(pathList);
}
缺点:
- 类加载方案需要重启App 后ClassLoader重新加载新类
- 因为类无法被卸载,想要重新加载新的类就需要重启App
- 因此采用类加载方案的热修复框架不能即时生效
- 如:QQ空间的超级补丁 ,Nuwa ,微信的Tinker ,饿了么Amigo 都是使用通过操作Element 数组实现的
Instant Run (立即更新)
什么是Instant Run
Instant Run 是Android Studio 2.0 以后新增的一个运行机制,能够显著减少开发人员第二次以及以后的构建和部署时间
使用Instant Run 前后编译部署应用程序流程的区别
- 之前:代码更改 -> 构建和部署app -> app销毁->app 重启 -> activity重启->更改的代码运行
- 之后:代码更改->构建改变部分->部署更改部分->(Cold Swap<App 重启>, Hot Swap ,Warm Swap<Activity 重启>)->更改的代码运行
- 传统的编译部署需要重新安装App和重启App,会比较耗时
Instant Run 的构建和部署都是基于更改的部分
Instant Run 原理
Instant Run原理就是:Instant Run 在第一次构建 APK 时,使用 ASM 在每一个方法中注入了判断代码
ASM :是一个 Java 字节码操控框架,它能够动态生成类或者增强现有类的功能。 ASM 可以直接产生 clsss文件,也可以在类被加载到虚拟机之前动态改变类的行为。
通过Instant Run进行热更新的步骤
- 通过一些工具在类被加载到虚拟机之前动态的在类中每个方法上注入判断代码
- 注入的代码内容大致为:是否需要加载补丁
- 如果需要加载补丁,使用DexClassLoader类加载器加载指定地址的修复包,然后用DexClassLoader 的 loadClass方法加载补丁类,再通过反射使用这个被加载的补丁类
怎么使用Instant Run进行动态更新的 (以美团热更新Robust 为例简单列举下)
实现原理可以围绕着两点
- 代码注入
- 替换老的逻辑(加载补丁包)
Robust 中的几个重要类介绍:
PatchesInfo 接口 (用于保存修复类和旧类的类信息)
- 补丁包说明类,可以获取所有补丁对象,每个对象包含被修复类名及该类对应的补丁类
- 每个修复包中必须有一个类需要实现这个,用于存放此修复包中所有需要修复的类信息
- 通过这个接口获取到指定修复的类和旧类信息
PatchedClassInfo
- private String patchedClassName;//需要修复的类名称
- private String patchClassName;//patch中的补丁类名称
- 存放已修复类和旧类的类名,用于后续的动态加载
ChangeQuickRedirect 接口
- 每个补丁类必须实现ChangeQuickRedirect接口,内部有两个方法
- isSupport 判断当前方法是否执行补丁逻辑
- accessDispatch具体修复逻辑
PatchProxy
此类是对ChangeQuickRedirect修复类做了一层包装,最终还是调用的ChangeQuickRedirect实现类中的方法
实现 ChangeQuickRedirect 的类
每个补丁类必须实现的接口(ChangeQuickRedirect),内部有两个方法
两个方法
- isSupport 判断当前方法是否执行补丁逻辑
- accessDispatch具体修复逻辑
方法参数
- 第一个参数是方法的签名,这个签名的格式很简单:方法所属类全称:方法名:方法是否为static类型,注意中间使用冒号进行连接的。
- 第二个参数是方法的参数信息,而对于这个参数后面分析动态插入代码逻辑的时候会发现操作非常麻烦,才把这个参数弄到手的。
Robust 的实现为:
- 可以看到Robust为每个class增加了一个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,
- 当changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑
代码注入
原方法
public long getIndex() {
return 100;
}
将方法中注入判断代码后
public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex() {
if(changeQuickRedirect != null) {
//PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
}
}
return 100L;
}
加载补丁包
重点方法在PatchExecutor 中的patch 方法
public class PatchExecutor extends Thread {
protected Context context;
protected PatchManipulate patchManipulate;
protected RobustCallBack robustCallBack;
public PatchExecutor(Context context, PatchManipulate patchManipulate, RobustCallBack robustCallBack) {
this.context = context.getApplicationContext();
this.patchManipulate = patchManipulate;
this.robustCallBack = robustCallBack;
}
@Override
public void run() {
try {
//拉取补丁列表
List<Patch> patches = fetchPatchList();
//应用补丁列表
applyPatchList(patches);
} catch (Throwable t) {
Log.e("robust", "PatchExecutor run", t);
robustCallBack.exceptionNotify(t, "class:PatchExecutor,method:run,line:36");
}
}
/**
* 拉取补丁列表
*/
protected List<Patch> fetchPatchList() {
return patchManipulate.fetchPatchList(context);
}
/**
* 应用补丁列表
*/
protected void applyPatchList(List<Patch> patches) {
if (null == patches || patches.isEmpty()) {
return;
}
Log.d("robust", " patchManipulate list size is " + patches.size());
for (Patch p : patches) {
if (p.isAppliedSuccess()) {
Log.d("robust", "p.isAppliedSuccess() skip " + p.getLocalPath());
continue;
}
if (patchManipulate.ensurePatchExist(p)) {
boolean currentPatchResult = false;
try {
currentPatchResult = patch(context, p);
} catch (Throwable t) {
robustCallBack.exceptionNotify(t, "class:PatchExecutor method:applyPatchList line:69");
}
if (currentPatchResult) {
//设置patch 状态为成功
p.setAppliedSuccess(true);
//统计PATCH成功率 PATCH成功
robustCallBack.onPatchApplied(true, p);
} else {
//统计PATCH成功率 PATCH失败
robustCallBack.onPatchApplied(false, p);
}
Log.d("robust", "patch LocalPath:" + p.getLocalPath() + ",apply result " + currentPatchResult);
}
}
}
protected boolean patch(Context context, Patch patch) {
if (!patchManipulate.verifyPatch(context, patch)) {
robustCallBack.logNotify("verifyPatch failure, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:107");
return false;
}
ClassLoader classLoader = null;
try {
File dexOutputDir = getPatchCacheDirPath(context, patch.getName() + patch.getMd5());
classLoader = new DexClassLoader(patch.getTempPath(), dexOutputDir.getAbsolutePath(),
null, PatchExecutor.class.getClassLoader());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
if (null == classLoader) {
return false;
}
Class patchClass, sourceClass;
Class patchesInfoClass;
PatchesInfo patchesInfo = null;
try {
Log.d("robust", "patch patch_info_name:" + patch.getPatchesInfoImplClassFullName());
patchesInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
patchesInfo = (PatchesInfo) patchesInfoClass.newInstance();
} catch (Throwable t) {
Log.e("robust", "patch failed 188 ", t);
}
if (patchesInfo == null) {
robustCallBack.logNotify("patchesInfo is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:114");
return false;
}
//classes need to patch 1.获取补丁包中所有待修复类信息
List<PatchedClassInfo> patchedClasses = patchesInfo.getPatchedClassesInfo();
if (null == patchedClasses || patchedClasses.isEmpty()) {
// robustCallBack.logNotify("patchedClasses is null or empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:122");
//手写的补丁有时候会返回一个空list
return true;
}
boolean isClassNotFoundException = false;
for (PatchedClassInfo patchedClassInfo : patchedClasses) {
String patchedClassName = patchedClassInfo.patchedClassName;
String patchClassName = patchedClassInfo.patchClassName;
if (TextUtils.isEmpty(patchedClassName) || TextUtils.isEmpty(patchClassName)) {
robustCallBack.logNotify("patchedClasses or patchClassName is empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:131");
continue;
}
Log.d("robust", "current path:" + patchedClassName);
try {
try {
//2.加载要被修复的类
sourceClass = classLoader.loadClass(patchedClassName.trim());
} catch (ClassNotFoundException e) {
isClassNotFoundException = true;
// robustCallBack.exceptionNotify(e, "class:PatchExecutor method:patch line:258");
continue;
}
Field[] fields = sourceClass.getDeclaredFields();
Log.d("robust", "oldClass :" + sourceClass + " fields " + fields.length);
Field changeQuickRedirectField = null;
for (Field field : fields) {
if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), sourceClass.getCanonicalName())) {
//3.找到要被修复类的注入的静态变量
changeQuickRedirectField = field;
break;
}
}
if (changeQuickRedirectField == null) {
robustCallBack.logNotify("changeQuickRedirectField is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:147");
Log.d("robust", "current path:" + patchedClassName + " something wrong !! can not find:ChangeQuickRedirect in" + patchClassName);
continue;
}
Log.d("robust", "current path:" + patchedClassName + " find:ChangeQuickRedirect " + patchClassName);
try {
//4.加载要修复类对应的patch中的补丁类对象
patchClass = classLoader.loadClass(patchClassName);
Object patchObject = patchClass.newInstance();
changeQuickRedirectField.setAccessible(true);
changeQuickRedirectField.set(null, patchObject);//5.将静态变量的值设置为补丁包中的补丁类
//patchObject为补丁类对象
Log.d("robust", "changeQuickRedirectField set success " + patchClassName);
} catch (Throwable t) {
Log.e("robust", "patch failed! ");
robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:163");
}
} catch (Throwable t) {
Log.e("robust", "patch failed! ");
// robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:169");
}
}
Log.d("robust", "patch finished ");
if (isClassNotFoundException) {
return false;
}
return true;
}
private static final String ROBUST_PATCH_CACHE_DIR = "patch_cache";
/*
* @param c
* @return 返回缓存补丁路径,一般是内部存储,补丁目录
*/
private static File getPatchCacheDirPath(Context c, String key) {
File patchTempDir = c.getDir(ROBUST_PATCH_CACHE_DIR + key, Context.MODE_PRIVATE);
if (!patchTempDir.exists()) {
patchTempDir.mkdir();
}
return patchTempDir;
}
}
重点代码
- 注解0处: 用DexClassLoader加载补丁包中的 PatchesInfoImpl 类,通过反射拿到这个 PatchesInfoImpl 实例对象。
- 注释1处:通过PatchesInfoImpl获取补丁包中所有待修复类信息
- 注释2处:用DexClassLoader加载要被修复的类
- 注释3处:找到要被修复类的注入的静态变量
- 注释4处:用DexClassLoader加载要修复类对应的patch中的补丁类对象
- 注释5处:将静态变量的值设置为补丁包中的补丁类,代码中patchObject为补丁类对象
加载补丁包原理
- 使用DexClassLoader加载指定地址的修复包
- 然后用DexClassLoader 的 loadClass方法加载补丁类,new出新对象,
- 在用反射把这新的补丁对象设置到旧类的changeQuickRedirect静态变量中即可。