您的位置:首页 > 移动开发 > Android开发

Android热补丁动态修复技术(一):从Dex分包原理到热补丁

2017-08-07 19:25 465 查看


一、参考

博文:安卓App热补丁动态修复技术介绍——by
QQ空间终端开发团队 

博文:Android dex分包方案——by 猫的午后 

开源项目:https://github.com/jasonross/Nuwa 

开源项目:https://github.com/dodola/HotFix 
http://blog.csdn.net/u010386612/article/details/50885320
感谢以上几位大神分享的技术知识!

关于热补丁技术,以上文章已经做了很详细的描述。但是细节上的东西都一带而过,这里会做出更为详细的说明,更适合初学者学习这门技术。


二、Dex分包方案的由来


2.1 Dalvik限制

众所周知,当apk解压后里面是只有一个classes.dex文件的,而这个dex文件里面就包含了我们项目的所有.class文件。

但是当一个app功能越来越复杂,可能会出现两个问题:
编译失败,因为一个dvm中存储方法id用的是short类型,导致dex中方法不能超过65536个
你的apk在android 2.3之前的机器无法安装,因为dex文件过大(用来执行dexopt的内存只分配了5M)


2.2 解决方案

针对上述两个问题,有人研究出了dex分包方案。 

原理就是将编译好的class文件拆分打包成两个dex,绕过dex方法数量的限制以及安装时的检查,在运行时再动态加载第二个dex文件中。

除了第一个dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以资源的方式放在安装包中,并在Application的onCreate回调中被注入到系统的ClassLoader。因此,对于那些在注入之前已经引用到的类(以及它们所在的jar),必须放入第一个Dex文件中。


三、Dex分包的原理——ClassLoader

接下来我们就来看看,如何将第二个dex文件注入到系统中。


3.1 ClassLoader体系

我们都知道,Java执行程序的时候是需要先将字节码加载到jvm之后才会被执行的,而这个过程就是使用到了ClassLoader类加载器。Android也是如此

以下是DVM的ClassLoader体系



查看官方文档可以知道以下两点: 

1.android系统是通过PathClassLoader加载系统类和已安装的应用的。 
Android uses this class for its system class loader and for its application class loader(s).


2.而DexClassPath则可以从一个jar包或者未安装的apk中加载dex 
A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of
an application.


从这里就可以看出,动态加载dex的时候我们应该使用DexClassLoader


3.2 ClassLoader源码分析

源码可以到这个网站查阅:http://androidxref.com/

DexClassLoader和PathClassLoader都只重写了BaseDexClassLoader的构造而已,而具体的加载逻辑则在BaseDexClassLoader中。

这部分源码都很简单,请务必看懂

BaseDexClassLoader部分源码:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;

/**
* Constructs an instance.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; may be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

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

从源码得知,当我们需要加载一个class时,实际是从pathList中去找的,而pathList则是DexPathList的一个实体。

DexPathList部分源码:
/*package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final String APK_SUFFIX = ".apk";

/** class definition context */
private final ClassLoader definingContext;

/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private final Element[] dexElements;

/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
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;
}
}
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
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

从这段源码可以看出,dexElements是用来保存dex的数组,而每个dex文件其实就是DexFile对象。遍历dexElements,然后通过DexFile去加载class文件,加载成功就返回,否则返回null

通常情况下,dexElements数组中只会有一个元素,就是apk安装包中的classes.dex 
而我们则可以通过反射,强行的将一个外部的dex文件添加到此dexElements中,这就是dex的分包原理了。 

这也是热补丁修复技术的原理。


四、热补丁修复技术的原理

上面的源码,我们注意到一点,如果两个dex中存在相同的class文件会怎样? 

先从第一个dex中找,找到了直接返回,遍历结束。而第二个dex中的class永远不会被加载进来。 
简而言之,两个dex中存在相同class的情况下,dex1的class会覆盖dex2的class。 

盗一下QQ空间的图,如图:classes1.dex中的Qzone.class并不会被加载 



而热补丁技术则利用了这一特性,当一个app出现bug的时候,我们就可以将出现那个bug的类修复后,重新编译打包成dex,插入到dexElements的前面,那么出现bug的类就会被覆盖,app正常运行,这就是热修复的原理了。 




五、本章结束

这章为大家介绍了热补丁技术的原理,但是大家可能并不会实际操作。 

1. 怎么通过反射将dex插入到elements 

2. 怎么讲修复后的类打包成dex 

这将是下一篇博客的内容,感谢阅读。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  热修复
相关文章推荐