前言
大家好,我是猪猪侠。热修复是近几年比较火的话题,它可以在不更新版本的前提下,使用户在无感的情况下修复应用的bug。从而避免了因发因版本时间较长,而导致因为bug对用户的体验感大大降低情况。
市面上比较有名的热修复框架
其中AndFix(阿里)已经不再维护了。今天我们来看下QZone热修复原理,相对于其他三种热修复框架,QZone热修复相对较简单,也比较容易理解。
类加载
我们知道普通的java程序是使用ClassLoader执行class文件,而Android虽然是使用java开发,但是Android却不能直接执行class文件,而是执行的dex文件。所以加载dex就需要一些特殊的类加载器。Android中常见的类加载器有BootClassLoader、BaseDexClassLoader、PathClassLoader、DexClassLoader。
BootClassLoader是加载Android系统源码,例如Activity,AMS等。PathClassLoader和DexClassLoader都是继承于BaseDexClassLoader,两者的区别在于构造方法参数不同。默认情况下,PathClassLoader是用于加载三方库,比如AppCompatActivity等这些代码。DexClassLoader是加载外部的dex文件,其实使用PathClassLoader去加载外部的dex文件也是没问题的。
双亲委托机制
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//获取已经加载过的Class
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//父亲加载过的Class
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.
//自己加载Class
c = findClass(name);
}
}
return c;
}
从上述源码我们可以看出,类加载的大致流程如下:
- 获取已经加载过的Class。
- 从父类加载器中获取Class。
- 如果parent为null,则调用BootstrapClassLoader进行加载。
- 如果class依旧没有找到,则调用当前类加载器的findClass方法进行加载。
双亲委托机制的优点
- 避免重复的类加载,可以节省资源(这也是热修复的前提)
- 安全性较高,防止核心API被随意篡改
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;
}
}
final class DexPathList{
//dex文件数组
private Element[] dexElements;
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* Open all files and load the (direct or contained) dex files up front.
*/
for (File file : files) {
if (file.isDirectory()) {
// We support directories for looking up resources. Looking up resources in
// directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
DexFile dex = null;
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if
* the zip file turns out to be resource-only (that is, no classes.dex file
* in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
}
原理分析
如上图所示,类加载会依次从class.dex到class2.dex。例如class.dex和class.dex中都有个类A,但是类只会从class.dex加载一次A类,后面class1.dex中的A类不会被加载。
如上图所以,假设class1.dex中有个存在Bug的A.class,那我们就可以将修复后的类插到Element数组的最前面,那当程序运行时就只会加载fix.dex中的A.class,而不会加载有Bug的A.class了。
那我们现在要做的就是修改将fix.dex插入到DexPathList中的dexElements数组的索引第一位,然后替换原来的dexElements数组。
那用什么方式呢?没错,就是强大的反射。下面看下主要代码
fun install(){
......
//将新的dex转换为dexElements数组
val makePathElements =
makePathElements(pathList, list, context.cacheDir, exceptions)
val newElements = java.lang.reflect.Array.newInstance(
dexElements.javaClass.componentType,
dexElements.size + makePathElements.size
)
//将新的dexElements和旧的dexElement数组合并
System.arraycopy(makePathElements, 0, newElements, 0, makePathElements.size)
System.arraycopy(
dexElements,
0,
newElements,
makePathElements.size,
dexElements.size
)
//将合并后的dexElements数组重新设置给DexPathList中的dexElements
dexElementsField.set(pathList, newElements)
}
/**
* 将dex转化为Element数组
*/
@Suppress("UNCHECKED_CAST")
@Throws(
IllegalAccessException::class,
InvocationTargetException::class,
NoSuchMethodException::class
)
private fun makePathElements(
dexPathList: Any,
files: ArrayList<File>,
optimizedDirectory: File,
exceptions: ArrayList<IOException>
): Array<Any> {
val makePathElementsMethod = dexPathList.javaClass.getDeclaredMethod(
"makePathElements",
List::class.java,
File::class.java,
List::class.java
)
if (!makePathElementsMethod.isAccessible) {
makePathElementsMethod.isAccessible = true
}
return makePathElementsMethod.invoke(
dexPathList,
files,
optimizedDirectory,
exceptions
) as Array<Any>
}
存在的问题
- 版本兼容,Android每个版本系统源码都有所不同,所以反射的时候需要对每个版本进行兼容。
- CLASS_ISPREVERIFIED标志问题。例如MainAcivity和Utils类存在于同一个dex中,这个时候MainActivity会被打上CLASS_ISPREVERIFIED标志,大概意思就是当MainActivity使用Utils类的时候,会直接从该dex中加载,而不会从其他dex中加载,这个时候就会出现问题。具体解决方案请参考安卓App热补丁动态修复技术介绍
- Android N混合编译与对热补丁影响解析
总结
这篇文章只是说了QZone热修复的大概原理,以及手撸了一个简单的热修复Demo,不可用于真实项目中,只是为了让自己加深对类加载和热修复原理的印象。好了,今天就到这里了,大家再见。