在开发中我们会遇到如下的情况:
1.刚发布的版本出现了严重的bug,这就需要去解决bug、测试并打渠道包在各个应用市场上重新发布,这会耗费大量的人力物力,代价会比较大。
2.已经改正了此前发布版本的bug,如果下一个版本是一个大版本,那么两个版本的间隔时间会很长,这样要等到下个大版本发布再修复bug,这样此前版本的bug会长期的影响用户。
3.版本升级率不高,并且需要很长时间来完成版本覆盖,此前版本的bug就会一直影响不升级版本的用户。
4.有一个小而重要的功能,需要短时间内完成版本覆盖,比如节日活动。
为了解决上面的问题,热修复框架就产生了。对于Bug的处理,开发人员不要过于依赖热修复框架,在开发的过程中还是要按照标准的流程做好自测、配合测试人员完成测试流程。
定义
热修复说白了就是”打补丁”,比如你们公司上线一个app,用户反应有重大bug,需要紧急修复。如果按照通 常做法,那就是程序猿加班搞定bug,然后测试,重新打包并发布。这样带来的问题就是成本高,效率低。于是,热 修复就应运而生.一般通过事先设定的接口从网上下载无Bug的代码来替换有Bug的代码。这样就省事多了,用 户体验也好。
用户不用重新下载一个新的apk安装,而是直接下载一个补丁包,通过补丁来替换一些出现bug的类, 当然下载补丁的过程用户一般是感觉不到的,表面上看是直接修复了bug。
在早期的android系统中,为了优化dex,所有的method会存放在一张表里面,表的大小位short,也就是65535(65K)现在android代码非常多,超过65K很正常,这个时候就需要一种解决方案来解决这个问题。简单来说就是将编译好的class文件分拆成2个dex文件,绕过65k的限制。
虽然热修复框架很多,但热修复框架的核心技术主要有三类,分别是代码修复、资源修复和动态链接库修复。
1.Android的类加载机制
java中的ClassLoader是加载class文件,而Android中的虚拟机无论是dvm还是art都只能识别dex文件。因此Java中的ClassLoader在Android中不适用。Android中的java.lang.ClassLoader这个类也不同于Java中的java.lang.ClassLoader。
BootClassLoader
BootClassLoader实例在Android系统启动的时候被创建,用于加载一些Android系统框架的类,其中就包括APP用到的一些系统类。BootClassLoader是ClassLoader的一个内部类。与Java中的Bootstrap ClassLoader不同的是,它并不是由C/C++代码实现,而是由Java实现的。
PathClassLoader:
用来加载系统类和应用类。
继承自BaseDexClassLoader。
在应用启动的时候创建PathClassLoader实例,只能加载系统中已经安装过的apk;
官方文档对PathClassLoader描述,大概意思是:
PathClassLoader简单实现了ClassLoader,可以操作本地文件系统的文件和目录,但是不能从网络中加载类。Android使用PathClassLoader作为系统类加载器和应用程序类加载器。
DexClassLoader:
用来加载jar、apk、dex文件,加载jar、apk也是最终抽取里面的Dex文件进行加载,可以从SD卡中加载未安装的apk。
PathClassLoader和DexClasLoader都是继承自 dalviksystem.BaseDexClassLoader,它们的类加载逻辑全部写在BaseDexClassLoader中。
上图展示了Android中的ClassLoader中的继承体系,其中SecureClassLoader和UrlClassLoader是在Java中的类加载器,在Android中是没法办使用的。
2.热修复机制
在Android中我们主要关心的是PathClassLoader和DexClassLoader。
PathClassLoader用来操作本地文件系统中的文件和目录的集合。并不会加载来源于网络中的类。Android采用这个类加载器一般是用于加载系统类和它自己的应用类。这个应用类放置在data/data/包名下。
看一下PathClassLoader的源码,只有2个构造方法:
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
DexClassLoader源码
DexClassLoader可以加载一个未安装的APK,也可以加载其它包含dex文件的JAR/ZIP类型的文件。DexClassLoader需要一个对应用私有且可读写的文件夹来缓存优化后的class文件。而且一定要注意不要把优化后的文件存放到外部存储上,避免使自己的应用遭受代码注入攻击。看一下它的源码,只有1个构造方法:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
两个ClassLoader就两三行代码,只是调用了父类的构造函数.
可以看到,PathClassLoader和DexClassLoader除了构造方法传参不同,其它的逻辑都是一样的。要注意的是DexClassLoader构造方法第2个参数指的是dex优化缓存路径,这个值是不能为空的。而PathClassLoader对应的dex优化缓存路径为null是因为Android系统自己决定了缓存路径。
接下来我们看一下BaseDexClassLoader这个类:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
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;
}
BaseDexClassLoader的构造方法有四个参数:
dexPath,指的是在Androdi包含类和资源的jar/apk类型的文件集合,指的是包含dex文件。多个文件用“:”分隔开,用代码就是File.pathSeparator。
optimizedDirectory,指的是odex优化文件存放的路径,可以为null,那么就采用默认的系统路径。
libraryPath,指的是native库文件存放目录,也是以“:”分隔。
parent,parent类加载器
在BaseDexClassLoader 构造函数中创建一个DexPathList类的实例,这个DexPathList的构造函数会创建一个dexElements 数组也来存放dex文件。
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//创建一个数组
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
...
}
然后BaseDexClassLoader 重写了findClass方法,调用了pathList.findClass,跳到DexPathList类中.
/* package */final class DexPathList {
...
public Class findClass(String name, List<Throwable> suppressed) {
//遍历该数组
for (Element element : dexElements) {
//初始化DexFile
DexFile dex = element.dexFile;
if (dex != null) {
//调用DexFile类的loadClassBinaryName方法返回Class实例
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
...
}
pathList.findClass()会遍历这个数组,然后初始化DexFile,如果DexFile不为空那么调用DexFile类的loadClassBinaryName方法返回Class实例.
归纳上面的话就是:
ClassLoader会遍历这个数组,然后加载这个数组中的dex文件. 而ClassLoader在加载到正确的类之后,就不会再去加载有Bug的那个类了,我们把这个正确的类放在Dex文件中,让这个Dex文件排在dexElements数组前面即可.
热修复就是利用android中的 DexClassLoader 类加载器,动态加载补丁dex,替换有bug的类。
加载类的过程
跟Java类加载器加载类类似,都是通过ClassLoader用loadClass方法来加载我们需要的类,但是它们两的代码实现不太一样。
下面是Android中ClassLoader的loadClass方法代码:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
从loadClass可以看出,与Java类似,都是使用双亲委托机制来加载类。
加载过程:
1.会先查找当前ClassLoader是否加载过此类,有就返回;
2.如果没有,查询父ClassLoader是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;
3.如果整个类加载器体系上的ClassLoader都没有加载过,才由当前ClassLoader加载,整个过程类似循环链表一样。
双亲委托机制的作用
1.共享功能,一些Framework层级的类一旦被顶层的ClassLoader加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。
2.隔离功能,保证java/Android核心类库的纯净和安全,防止恶意加载。
使用ClassLoader一些需要注意的问题
我们都知道,我们可以通过动态加载获得新的类,从而升级一些代码逻辑,这里有几个问题要注意一下。
如果你希望通过动态加载的方式,加载一个新版本的dex文件,使用里面的新类替换原有的旧类,从而修复原有类的BUG,那么你必须保证在加载新类的时候,旧类还没有被加载,因为如果已经加载过旧类,那么ClassLoader会一直优先使用旧类。
如果旧类总是优先于新类被加载,我们也可以使用一个与加载旧类的ClassLoader没有树的继承关系的另一个ClassLoader来加载新类,因为ClassLoader只会检查其Parent有没有加载过当前要加载的类,如果两个ClassLoader没有继承关系,那么旧类和新类都能被加载。
不过这样一来又有另一个问题了,在Java中,只有当两个实例的类名、包名以及加载其的ClassLoader都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的ClassLoader不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。
总结
Android中的类加载器是BootClassLoader、PathClassLoader、DexClassLoader,其中BootClassLoader是虚拟机加载系统类需要用到的,PathClassLoader是App加载自身dex文件中的类用到的,DexClassLoader可以加载直接或间接包含dex文件的文件,如APK等。
无论是热修复还是插件化技术中都利用了类加载机制,所以深入理解Android中的类加载机制对于理解这些技术的原理很有帮助。
Android热修复(2):AndFix热修复框架的使用
Android热修复(3):Tinker的使用