关于热修复其实很早都想动手写,不过由于没研究过具体的实践,不敢乱谈。
那么何为热修复呢?所谓热修复,无非是线上出了bug,开发人员可以发补丁,应用程序默默下载好对应问题的补丁,修复这个bug。这种热修复其实很适合client-server的模式,当然了客户端肯定也是适用的。

热修复/热部署 最早使用在web后端的,至于客户端热修复,最早听到热修复是2014年,当时在阿里技术嘉年华分享会上,阿里分享的插件化部署专题中讲到其中的热修复。反而在去年(2015年),出了很多热修复和插件化的框架,可以说是插件化/热部署元年。

虽然我们落后了半年,所以赶快补上热修复这一章。

为什么需要热修复

上周去百度,和鸿洋有过交流,当时,鸿洋大神说:热修复只能在国内玩,国外都是Google play。
的确,目前热修复尽管有很多坑,做了好多工作,可能吃力不讨好,各种适配可能还是没修复线上的有些Bug。不过呢,对于一个产品有热修复毕竟是件好事。尤其是对于一个有众多用户的app(如支付宝、微信、手淘等),一个bug不只是影响到几个几十个用户,一些创业公司的APP,崩溃或者bug可能直接导致用户卸载和永不使用,所以,就冲它有不用发版也可以解决我们线上的bug,我们的app也要适当考虑加入热修复。

在IOS上,有JsPatch、有waxPatch,有些游戏公司自己搞了一个lua引擎放到应用里,搞一些类似动态部署的东西。
也可以采用其他方案,如RN,阿里的Weex等方案。

热修复

热修复,这个词是在去年QQ空间开发团队,发表的一篇文章安卓App热补丁动态修复技术介绍出现后,在”江湖”上引起了”动荡。Android程序员奔走相告–“我们终于找到梦寐以求的实现热修复的理论支持”。

可能还有其他方案如阿里的And-Fix,ClassLoader的替代方案。参考文章下有and-fix和ClassLoader的文章,记得点开阅读 。其中 markzhai也提到了现在classLoader替换方案已经成这一年多来新的变化。

安卓App热补丁动态修复技术介绍建议大家多看几遍,一遍远远不够的。

这个链接你一定要点开
这个链接你一定要点开
这个链接你一定要点开
重要的事情说三遍!!!!!!

记得看下, 他是热修复的始祖级的文章,也是本文重点抄袭对象。

我们知道Android系统也是仿照java搞了一个虚拟机,不过它不叫JVM,它叫Dalvik/ART VM他们还是有很大区别的(这是不是我们的重点, 点开是个拓展阅读)。我们只需要知道,Dalvik/ART VM 虚拟机加载类和资源也是要用到ClassLoader,不过Jvm通过ClassLoader加载的class字节码,而Dalvik/ART VM通过ClassLoader加载则是dex。

ClassLoader

在Android中,我们常用的ClassLoader关系如上图,其中BaseDexClassLoader

其中DexClass可以加载apk,jar,及dex文件,但PathClassLoader只能加载已安装到系统中(即/data/app目录下)的apk文件。

Dex加载方式

就让我们来稍微寻找下热修复的突破口。

先来瞅瞅BaseDexClassLoader的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
**
* Base class for common functionality between various dex-based
* {@link ClassLoader} implementations.
*/
public class BaseDexClassLoader extends ClassLoader {
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;
}

5.0的DexPathList部分代码

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
final class DexPathList {
private static final String DEX_SUFFIX = ".dex";

private final Element[] dexElements;

/** List of native library directories. */
private final File[] nativeLibraryDirectories;
/**
* Constructs an instance.
*
* @param definingContext the context in which any as-yet unresolved
* classes should be defined
* @param dexPath list of dex/resource path elements, separated by
* {@code File.pathSeparator}
* @param libraryPath list of native library directory path elements,
* separated by {@code File.pathSeparator}
* @param optimizedDirectory directory where optimized {@code .dex} files
* should be found and written to, or {@code null} to use the default
* system directory for same
*/
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}

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;
}

DexFile部分源码

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
public final class DexFile {
private long mCookie;
private final String mFileName;
private final CloseGuard guard = CloseGuard.get();

……
public Class loadClass(String name, ClassLoader loader) {
String slashName = name.replace('.', '/');
return loadClassBinaryName(slashName, loader, null);
}
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, long cookie,
List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}

……
private static native Class defineClassNative(String name, ClassLoader loader, long cookie)
throws ClassNotFoundException, NoClassDefFoundError;
}

其中5.0的DexPathList源码中的dexElements(Line 4)就是我们Element(保存有dex信息)的数组。当需要寻找一个class时,BaseDexClassLoader会先调用BaseDexClassLoader中的pathList的findClass方法,而pathList实际上是一个DexPathList对象,查看DexPathList的源码发现,findClass方式其实是去遍历dexElements中的element元素,通过DexFile的对象去loadClass。

热修复就是利用dexElements的顺序来做文章,当一个补丁的patch.dex放到了dexElements的第一位,那么当加载一个bug类时,发现在patch.dex中,则直接加载这个类,原来的bug类可能就被覆盖了。

发版后发现class1.dex中的Bug.class有一个bug,修复后有一个修复好的patch.dex.

CLASS_ISPREVERIFIED问题

根据QQ空间谈到的在虚拟机启动的时候,在verify选项被打开的时候,如果static方法、private方法、构造函数等,其中的直接引用(第一层关系)到的类都在同一个dex文件中,那么该类就会被打上CLASS_ISPREVERIFIED标志,且一旦类被打上CLASS_ISPREVERIFIED标志其他dex就不能再去替换这个类。所以一定要想办法去阻止类被打上CLASS_ISPREVERIFIED标志。

为了阻止类被打上CLASS_ISPREVERIFIED标志,QQ空间开发团队提出了一个方法是先将一个预备好的hack.dex加入到dexElements的第一项,让后面的dex的所有类都引用hack.dex其中的一个类,这样原来的class1.dex、class2.dex、class3.dex中的所有类都引用了hack.dex的类,所以其中的都不会打上CLASS_ISPREVERIFIED标志。

我们可以参考dodola/HotFix项目来说明。

app中有一个LoadBugClass类,他引用了BugClass类。

1
2
3
4
5
6
public class LoadBugClass {
public String getBugString() {
BugClass bugClass = new BugClass();
return bugClass.bug();
}
}

其中BugClass出现了bug。按照刚才的理论LoadBugClass会被打成CLASS_ISPREVERIFIED标志。为了阻止它打上CLASS_ISPREVERIFIED标志,需要在他的构造函数里添加hack.dex一个类引用,如下所示:

1
2
3
4
5
LoadBugClass(){
…………

System.out.println(dodola.hackdex.AntilazyLoad.class)
}

其中AntilazyLoad就是hack.dex的类。这样处理不会增加方法数,对代码的侵入较少。

好了这节介绍了Android热修复的实现原理,下一篇会结合jasonross/Nuwadodola/HotFix来谈一下他们的实践。

参考文章


  1. QQ空间 – 安卓App热补丁动态修复技术介绍
  2. Android dex分包方案
  3. 张涛 – Android 热修复,没你想的那么难
  4. 鸿洋 –Android 热补丁动态修复框架小结
  5. Android HotFix方案
  6. classLoader 替换方案 Android 插件化原理解析——插件加载机制