一、简述
android热修复是这2年较火的新技术,是作为安卓工程师必学的技能之一。在以前,线上产品如果出现了一点bug,就只能在修复后重新打包测试然后审核上线,然后用户还得重新下载安装,费时费力,大大降低了用户体验,但是现在有了热修复方案。
目前较火的热修复方案有很多:
andfix,tinker,还有阿里最新的sophix方案(据称支持大多数加固)
本篇文章通过通俗易懂的方式来解析热修复基本原理,当然和上述成熟产品还有差距。
二、bug修复流程
当线上产品出现bug的时候,比如某个java文件代码写的不好出现了空指针,也就导致这个class文件有问题,所以要做的就是替换这个class文件。
- 下发补丁,也就是从服务端获取补丁包。
- 替换成新的class文件,实现bug修复。
如何才能替换class文件,就是修复的关键点。首先先来了解一下java的类加载器classLoader。
三、java类加载器
先来了解一下虚拟机,java使用的是jvm,而android使用的是经过优化的Dalvik虚拟机还有之后的ART。
- Dalvik执行.dex格式的字节码
- JVM执行.class格式的字节码。
由于执行格式不同,所以java使用的是classLoader来加载,而android需要使用专用的类加载器DexClassLoader和PathClassLoader。
PathClassLoader和DexClassLoader的区别
- PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
- DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。
来看看两者区别,直接贴代码:
// PathClassLoader
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);
}
}
// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
从源码来看两者都继承自BaseDexClassLoader,唯一区别是DexClassLoader传入了一个optimizedDirectory参数。然后再来看下BaseDexClassLoader的构造函数:
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
- dexPath:要加载的程序文件(一般是dex文件,也可以是jar/apk/zip文件)所在目
- optimizedDirectory:dex文件的输出目录(因为在加载jar/apk/zip等压缩格式的程序文件时会解压出其中的dex文件,该目录就是专门用于存放这些被解压出来的dex文件的)。
- libraryPath:加载程序文件时需要用到的库路径。
注意这里的pathList,在下面会进行解析
通过类加载器获取class
来看BaseDexClassLoader的findClass()方法,该方法用来获取加载到的class
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;
}
可以看到,实际是调用的是之前提到的pathList对象的findClass方法,所以先来看看DexPathList到底做了什么。
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
83 String libraryPath, File optimizedDirectory) {
84 if (definingContext == null) {
85 throw new NullPointerException("definingContext == null");
86 }
87
88 if (dexPath == null) {
89 throw new NullPointerException("dexPath == null");
90 }
91
92 if (optimizedDirectory != null) {
93 if (!optimizedDirectory.exists()) {
94 throw new IllegalArgumentException(
95 "optimizedDirectory doesn't exist: "
96 + optimizedDirectory);
97 }
98
99 if (!(optimizedDirectory.canRead()
100 && optimizedDirectory.canWrite())) {
101 throw new IllegalArgumentException(
102 "optimizedDirectory not readable/writable: "
103 + optimizedDirectory);
104 }
105 }
106
107 this.definingContext = definingContext;
108 ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
109 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
110 suppressedExceptions);
111 if (suppressedExceptions.size() > 0) {
112 this.dexElementsSuppressedExceptions =
113 suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
114 } else {
115 dexElementsSuppressedExceptions = null;
116 }
117 this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
118 }
主要看109行的makeDexElements方法,返回了一个dexElements数组,接下来得看该方法到底做了什么(方法中第一个参数方法splitDexPath(dexPath)实际是返回一个dex文件集合):
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
// 1.创建Element集合
ArrayList<Element> elements = new ArrayList<Element>();
// 2.遍历所有dex文件(也可能是jar、apk或zip文件)
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
...
// 如果是dex文件
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
// 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)
} else {
zip = file;
dex = loadDexFile(file, optimizedDirectory);
}
...
// 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
// 4.将Element集合转成Element数组返回
return elements.toArray(new Element[elements.size()]);
}
在这个方法中可以看出,把dex或压缩文件转成Element对象然后添加Element集合中最后转成数组形式返回。
findClass()
最后来看看DexPathList的findClass()方法:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
// 遍历出一个dex文件
DexFile dex = element.dexFile;
if (dex != null) {
// 在dex文件中查找类名与name相同的类
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
热修复实现原理
从最后的findClass方法可以看到,对Element数组进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。所以热修复的原理就是:
获取补丁的element数组,然后拿到app原来的element数组,合并成一个新的element数组,注意把补丁的数据放前面,最后把新的element数组通过反射的方式赋值给当前app的pathList的dexElement属性。因为修复好的class所在的element排在有bug的element前面,会先被拿到,所以实现了热修复。