个人博客

http://www.milovetingting.cn

热修复

前言

最近在熟悉Android热修复方面的知识,纸上得来终觉浅,因此写了一个基于dex分包方案的简单Demo。

热修复是什么

在热修复技术出现前,对于已经发布的应用,如果遇到BUG,需要再次发布版本,用户需要更新应用版本,才可以解决问题。这种方式,存在新版本覆盖所需要的时间较长、需要全量更新的问题。而基于热修复技术,可以打包出修复的补丁包,推送给客户端或者客户端拉取,可以减少修复BUG所需时间、减少更新包大小。

热修复

热修复分类

热修复

基于Dex分包的热修复方案原理

在Android中,类加载器的结构如下:

热修复

加载Dex的流程

PathClassLoader与DexClassLoader都可以加载Dex,但最终都是通过他们的父类BaseDexClassLoader的findClass方法加载的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PathClassLoader extends BaseDexClassLoader {

public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}

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

public class DexClassLoader extends BaseDexClassLoader {

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

BaseDexClassLoader中的findClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//BaseDexClassLoader中的代码
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;
}

可以看到,BaseDexClassLoader中的findClass方法又是通过DexPathList的findClass方法来具体实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//DexPathList中的代码

private Element[] dexElements;

public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;

if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

通过遍历dexElements中的元素来查找class,如果找到就不再往后查找。

1
2
3
4
5
6
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
//...
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
//...
}

dexElements是在构造方法中赋值的。

基于上面的分析,如果在dexElements数组的开始位置插入补丁dex,那么系统则会应用补丁包中的class,从而达到替换原来的class的效果。

由于dex在应用启动加载过后,不会再次重复加载。因此,这种方案只有在冷启动后,再次加载dex才会生效。

实现方案

在Application中,加载补丁dex,通过反射,将补丁dex插入到BaseDexClassLoader的属性:pathList中的dexElements数据开始位置。

实现代码:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class App extends Application {

@Override
public void onCreate() {
super.onCreate();
try {
PatchUtil.loadPatch(getApplicationContext(), "/sdcard/patch.dex");
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

public class PatchUtil {

/**
* 加载patch
*
* @param context
* @param patch
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public static void loadPatch(Context context, String patch) throws NoSuchFieldException,
IllegalAccessException {

//如果patch不存在,直接返回
File patchFile = new File(patch);
if (!patchFile.exists()) {
return;
}

//获取系统的PathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

//获取BaseDexClassLoader中DexPathList类型的属性:pathList
Field pathListField = pathClassLoader.getClass().getSuperclass().getDeclaredField(
"pathList");
pathListField.setAccessible(true);
Object pathListObject = pathListField.get(pathClassLoader);

//获取DexPathList中Element[]类型的dexElements
Field dexElementsField = pathListObject.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object dexElementsObject = dexElementsField.get(pathListObject);

//设置optimizedDirectory
File odex = context.getDir("odex", Context.MODE_PRIVATE);
//创建自定义的DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(patch, odex.getAbsolutePath(), null,
context.getClassLoader());
//获取BaseDexClassLoader中DexPathList类型的属性:pathList
Field patchPathListField = dexClassLoader.getClass().getSuperclass().getDeclaredField(
"pathList");
patchPathListField.setAccessible(true);
Object patchPathListObject = patchPathListField.get(dexClassLoader);

//获取DexPathList中Element[]类型的dexElements
Field patchDexElementsField = patchPathListObject.getClass().getDeclaredField(
"dexElements");
patchDexElementsField.setAccessible(true);
Object patchDexElementsObject = patchDexElementsField.get(patchPathListObject);

//合并数组
Class<?> elementClazz = dexElementsObject.getClass().getComponentType();
int dexElementsSize = Array.getLength(dexElementsObject);
int patchDexElementsSize = Array.getLength(patchDexElementsObject);
int newDexElementsSize = dexElementsSize + patchDexElementsSize;
Object newDexElements = Array.newInstance(elementClazz, newDexElementsSize);
for (int i = 0; i < newDexElementsSize; i++) {
if (i < patchDexElementsSize) {
Array.set(newDexElements, i, Array.get(patchDexElementsObject, i));
} else {
Array.set(newDexElements, i, Array.get(dexElementsObject,
i - patchDexElementsSize));
}
}

//替换原来的dexElements
dexElementsField.set(pathListObject, newDexElements);
}

}

模拟发布应用中出现的BUG

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
public class Foo {

/**
* 显示Toast
*
* @param context
* @param text
*/
public static void showToastShort(Context context, String text) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
}

}


public class MainActivity extends AppCompatActivity {

private Foo foo;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
foo = new Foo();
foo.showToastShort(getApplicationContext(), "出现BUG啦~~~");
}
}

生成修复补丁

1
2
3
4
5
6
7
8
9
10
11
12
public class MainActivity extends AppCompatActivity {

private Foo foo;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
foo = new Foo();
foo.showToastShort(getApplicationContext(), "BUG修复啦~~~");
}
}

在Android Studio中,先Build-Clean Project,然后Build-Rebuild Project,在项目的对应模块的\build\intermediates\javac\debug\classes目录下,将生成的对应class复制出来,放在其它位置,如D:\HotFix,复制出来的class文件要放在对应的包结构下,如:

热修复

使用SDK中自带的dx工具生成dex文件

打开CMD窗口,定位到SDK中的build-tools文件夹中对应的版本,如28.0.0

热修复

也可以将这个路径加入到系统的环境变量中,就可以在任何位置调用dx命令

输入以下命令生成dex:–dex –output=D:\HotFix\patch.dex D:\HotFix\

这里为简化操作,只是简单将文件推到/sdcard/下,对应具体的业务,可以通过网络下载回来。这里由于用到了sdcard,6.0以上的设备,需要申请存储的运行时权限。

结束应用的进程,再次打开应用,就会加载补丁dex,运行修复后的代码。

应用补丁前

热修复

应用补丁后

热修复

CLASS_ISPREVERIFIED问题

这个问题只在Dalvik虚拟机之下出现(Android 4.4以下默认使用dalvik,5.0以后默认使用art虚拟机)。出现的原因:

apk在安装时,Dalvik虚拟机如果发现一个类A引用了其它类B,如果这个类B和类A位于同一个dex里,那么类A就会打上CLASS_ISPREVERIFIED标记。因此,如果类A引用了一个有BUG的类C,修复时用multidex热修复方案加载一个patch.dex,由于这个类已经被打上标记,而重启应用后,再次加载dex时,这个类C又位于另一个dex中,程序就会报错。

目前网上用的比较多的解决方案是,在类的构造函数中动态引入一个位于其它dex中的类,即字节码插桩。这块内容在下篇文章会展现。

源码地址:https://github.com/milovetingting/Samples/tree/master/HotFix