Android热修复原理解析

概述

热修复即”打补丁“,当一个app上线后,如果发现重大的bug,需要紧急修复。常规的做法是修复bug,然后重新打包,再上线到各个渠道。这种方式的成本高,效率低。

于是热修复技术应运而生,热修复技术一般的做法是应用启动的时候,主动去服务端查询是否有补丁包,有就下载下来,并在下一次启动的时候生效,这样就可以快速解决线上的紧急bug。

Android中的热修复包括:代码修复资源修复动态链接库修复。本文主要讲解代码修复。

热修复原理

代码修复的原理主要是类替换。类的替换就涉及到ClassLoader的使用,Android中可用来动态加载代码的ClassLoader有PathClassLoaderDexClassLoader

因为PathClassLoader在Dalvik虚拟机中只能用来加载已安装apk的类,而DexClassLoader在Dalvik和ART虚拟机中都能加载未安装apk或者dex中的类,所以热修复使用DexClassLoader来加载补丁包中的类。

classloader

ClassLoader.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public abstract class ClassLoader {

// ...

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

// 使用双亲委托的机制进行类的加载
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,从缓存中查找类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 缓存找不到类,就委托给父加载器进行加载
c = parent.loadClass(name, false);
} else {
// 没有父加载器,则委托给顶级的BootstrapClassLoader进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// 如果还是没找到类,就主动从自己的加载路径中去查找
c = findClass(name);
}
}
return c;
}

// 这是ClassLoader主动加载类的方法,由子类具体实现
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

// ...

}

DexClassLoader.java

1
2
3
4
5
6
public class DexClassLoader extends BaseDexClassLoader {

public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}

BaseDexClassLoader.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BaseDexClassLoader extends ClassLoader {
// ...

// dex文件的路径列表
private final DexPathList pathList;

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
// ...
}

DexPathList.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final class DexPathList {
// ...

// 每个元素代表着一个dex
private Element[] dexElements;

public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}

if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

// ...
}

说明

通过上面几个类的关系,和类的查找过程,我们可以发现最终是通过遍历DexPathListdexElements数组进行类的查找加载,当找到类就返回;

dexElements数组的每个元素都代表着一个dex文件,所以为了让补丁包中要替换的类抢先于有bug的类被加载,就需要将补丁包dex插入到dexElements数组的头部。

热修复实战

生成补丁dex文件

Step1. 修改待修复的Title类;

1
2
3
4
5
6
7
8
9
10
11
package com.github.xch168.hotfixdemo;

/**
* Created by XuCanHui on 2019/1/29.
*/
public class Title {

public String getTitle() {
return "hotfix title";
}
}

Step2. 编译Title类

1
javac com/github/xch168/hotfixdemo/Title.java

Step3. 用d8命令将Title.class打包成patch.dex

d8命令的位置:<sdk-dir>/build-tools/<versionName>

1
d8 Title.class

命令执行完后就会生成一个classes.dex文件,将其重命名为patch.dex

Step4. 将patch.dex上传到七牛云的对象存储服务器上。

patch.dex在七牛对象存储服务器上的外链:http://pm3fh7vxn.bkt.clouddn.com/patch.dex

qiniuoss

下载补丁包patch.dex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class HotfixHelper {

public static void loadPatch(Context context, OnPatchLoadListener listener) {
File patchFile = new File(context.getCacheDir() + "/patch.dex");
if (patchFile.exists()) {
patchFile.delete();
}

downloadPatch(patchFile, listener);
}

private static void downloadPatch(final File patchFile, final OnPatchLoadListener listener) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("http://pm3fh7vxn.bkt.clouddn.com/patch.dex")
.get()
.build();
client.newCall(request)
.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
if (listener != null) {
listener.onFailure();
}
e.printStackTrace();
}

@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.code() == 200) {
FileOutputStream fos = new FileOutputStream(patchFile);
fos.write(response.body().bytes());
fos.close();
if (listener != null) {
listener.onSuccess();
}
} else {
if (listener != null) {
listener.onFailure();
}
}
}
});
}
}

应用补丁包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class HotfixHelper {

public static void applyPatch(Context context) {
// 获取宿主的ClassLoader
ClassLoader classLoader = context.getClassLoader();
Class loaderClass = BaseDexClassLoader.class;
try {
// 获取宿主ClassLoader的pathList对象
Object hostPathList = ReflectUtil.getField(loaderClass, classLoader, "pathList");
// 获取宿主pathList对象中的dexElements数组对象
Object hostDexElement = ReflectUtil.getField(hostPathList.getClass(), hostPathList, "dexElements");

File optimizeDir = new File(context.getCacheDir() + "/optimize");
if (!optimizeDir.exists()) {
optimizeDir.mkdir();
}
// 创建补丁包的类加载器
DexClassLoader patchClassLoader = new DexClassLoader(context.getCacheDir() + "/patch.dex", optimizeDir.getPath(), null, classLoader);
// 获取补丁ClassLoader中的pathList对象
Object patchPathList = ReflectUtil.getField(loaderClass, patchClassLoader, "pathList");
// 获取补丁pathList对象中的dexElements数组对象
Object patchDexElement = ReflectUtil.getField(patchPathList.getClass(), patchPathList, "dexElements");

// 合并宿主中的dexElements和补丁中的dexElements,并把补丁的dexElements放在数组的头部
Object newDexElements = combineArray(hostDexElement, patchDexElement);
// 将合并完成的dexElements设置到宿主ClassLoader中去
ReflectUtil.setField(hostPathList.getClass(), hostPathList, "dexElements", newDexElements);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

/**
*
* @param hostElements 宿主中的dexElements
* @param patchElements 补丁包中的dexElements
* @return Object 合并成的dexElements
*/
private static Object combineArray(Object hostElements, Object patchElements) {
Class<?> componentType = hostElements.getClass().getComponentType();
int i = Array.getLength(hostElements);
int j = Array.getLength(patchElements);
int k = i + j;
Object result = Array.newInstance(componentType, k);
// 将补丁包的dexElements合并到头部
System.arraycopy(patchElements, 0, result, 0, j);
System.arraycopy(hostElements, 0, result, j, i);
return result;
}
}

在Application中应用补丁包,这里是应用启动最新调用的地方。

1
2
3
4
5
6
7
8
9
10
11
public class App extends Application {

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);

if (HotfixHelper.hasPatch(base)) {
HotfixHelper.applyPatch(base);
}
}
}

测试流程

Step1. 启动应用,下载补丁包;

Step2. 杀掉应用,然后重启应用。

run


Demo地址:https://github.com/xch168/HotfixDemo

参考链接

  1. 一步步手动实现热修复(一)-dex文件的生成与加载
  2. 一步步手动实现热修复(二)-类的加载机制简要介绍
  3. 一步步手动实现热修复(三)-Class文件的替换
  4. Android热修复原理(一)热修复框架对比和代码修复
  5. Android 热修复,没你想的那么难
  6. HenCoderPlus
文章目录
  1. 1. 概述
  2. 2. 热修复原理
  3. 3. 热修复实战
    1. 3.1. 生成补丁dex文件
    2. 3.2. 下载补丁包patch.dex
    3. 3.3. 应用补丁包
    4. 3.4. 测试流程
  4. 4. 参考链接
,