Android热修复,在最近几年里已经不是什么新颖的技术了,很多公司都开始搞起了自己的热修复框架,最开始的像腾讯的Tinker,阿里的AndFix、Sophix,美团的Robust,想要自己实现一套热修复的框架,就需要了解其中的原理
1 热修复概念
什么是热修复,就是对线上版本的静默更新。当发布线上之后,如果出现了严重的bug,通常需要重新发版来修复,但是频繁地发版显然不是最佳解决方案,正常的发版流程是2个月发布一个新的版本,那么如果在用户无感知的情况下,修复线上bug,避免频繁地发版,热修复就随之出现了。 在开发端,通过Gradle插件生成补丁包,并上传到云端,客户端通过判断是否需要下载新的补丁包,并执行热修复
2 热修复使用到的技术
1 ClassLoader类加载机制 2 Dex动态加载技术 – hook反射 3 差分打包技术 – bsdiff 4 字节码插桩 – ASM Javassist 5 Gradle插件 – 发布差分包 6 so库的编译
3 几种常见的热修复框架对比
目前主流的热修复框架通常采用以下3种修复方式 1 native层hook Java层代码 bug fix 2 编译打包时字节码插桩 3 动态加载Dex文件,类加载技术
3.1 AndFix
AndFix是通过native层hook java层的代码,通常在native层实现热修复是不需要重启修复,这是即时生效的; 例如方法B中有bug,那么需要通过热修复替代这个方法;我们知道,所有方法的调用,都会在JVM中入栈,执行完成之后 出栈,方法在JVM中是一个ArtMethod结构体,那么在JVM运行这个方法之前,在Native层完成这个方法的替换,那么就完成了热修复的工作,而且是即时生效的
3.2 Robust
Robust采用的技术是编译时字节码插桩技术,这个过程在gradle-plugin中发生,在编译打包阶段,对每个函数注入一段逻辑代码,通过判断是否执行插入的这段代码,这个过程也是即时生效的;
3.3 Tinker
Tinker采用的是Dex动态加载技术,通过反射的方式,将待修复的类放在dexElements数组的前面,在类加载的时候,首先加载这个待修复的类,因为类加载机制不会重复加载类,达到修复的目的,但这个方式是需要重启生效的(出现bug的类在ClassLoader中是不能替换的,存在缓存中,只能重启重新进行类加载)
以上3种方式是目前热修复常见的3种方式,其实各有利弊,像native层处理需要大量的开发成本,跟Robust一样,只能达到修复bug的目的,不能新增类和轻量级的功能;而Tinker则是需要重启才能生效
4 类加载机制
Android应用和Java类的加载机制基本一致,Java类将代码编译成class文件,JVM加载class文件;而Android多出的一步就是将class文件转换为dex文件,通过dalvik或者Art虚拟机加载,Android也有自己的类加载器
4.1 Android类加载器
Android中有3个父类加载器,BootClassLoader、BaseDexClassLoader、URLClassLoader,其中BaseDexClassLoader有两个子类,PathClassLoader和DexClassLoader
PathClassLoader主要用于加载我们自己写的Java/Kotlin代码,只能加载已经安装的apk文件(data/app目录);而DexClassLoader则是能够加载指定目录的文件,除了apk,还有jar包,比PathClassLoader更灵活
在Android当中,使用最多的就是两个加载器,PathClassLoader(默认的加载器),BootClassLoader(PathClassLoader的父类加载器)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.tv_classloader).text = "${classLoader}"
findViewById<TextView>(R.id.tv_super_classloader).text = "${classLoader.parent}"
}
}
4.2 双亲委派
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
c = findClass(name);
}
}
return c;
}
Android类加载同样遵循双亲委派机制,当一个子类加载器加载这个类时,如果加载过了那么就直接返回字节码文件;如果没有加载过,首先会向父类请求是否加载过,如果加载过了那么就直接返回父类加载过的字节码文件;如果没有加载过,那么调用父类的loadClass,递归判断, 如果整个链路上都没有加载过,那么由当前类加载器调用findClass,从dex文件中找出并加载这个类
什么时候会触发类加载? · new XX 创建一个类 · 当静态方法或者静态变量被调用时 Class.property · 反射 · loadClass
误区: 这里有一个误区
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
虽然PathClassLoader是继承自BaseDexClassLoader,但是PathClassLoader的父类加载器并不是BaseDexClassLoader,从前面的例子中也可以看到,它的父类是parent,这里不要认为父类就是父类加载器,这是两个概念
4.3 dex加载
PathClassLoader的构造方法都是调用父类的构造方法,去BaseDexClassLoader看下源码
@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;
}
这里有一个类需要关注一下,pathList(DexPathList),是在BaseDexClassLoader的构造方法中完成了初始化
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
其中 dexPath:目标类所在的apk、dex或者jar文件的路径(SD卡也可以),这个路径可以是多个路径,使用分隔符:分开 librarySearchPath:加载程序文件时需要用到的so库的路径 parent:当前类加载器的父加载器
# DexPathList.java
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
······
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted)
}
在DexPathList的构造方法中,初始化了一个Element数组
# DexPathList.java / makeDexElements
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
for (File file : files) {
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
try {
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 {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
在这里,会将apk中的dex文件存放到dexElements数组当中,调用DexPathList的findClass方法,遍历dexElements数组,从数组中找到这个类然后加载
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
这个也是Tinker实现的原理,如果某个类出现bug,那么将这个类打成patch包,放在dexElements数组第一位,在加载这个类后,当执行到bug类时将不会重新加载,而是使用bug fix类
4.4 从源码看PathClassLoader和DexClassLoader的异同
先看一下两者构造方法的异同
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);
}
}
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
可以看出,DexClassLoader多了一个构造参数optimizedDirectory,用于存放优化后的dex文件,路径可以为空
在DexPathList的makeDexElements方法中,对于dex文件,需要调用loadDexFile方法来生成一个DexFile
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,Element[] elements) throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}
这里会判断optimizedDirectory是否为空,在PathClassLoader中传入的参数为空,那么在DexClassLoader中传入了这个路径,会调用DexFile的loadDex方法
因为apk其实也是一个压缩文件zip包,像第一次启动时,PathClassLoader会将apk解压存在/data/dalvik-cache目录下,而使用DexClassLoader则是会将apk中可运行的文件提取出来,存放在optimizedDirectory路径下,那么应用程序启动时将会加载optimizedDirectory下的文件,启动速度更快,这就是odex优化
|