赞
踩
Android热修复,在最近几年里已经不是什么新颖的技术了,很多公司都开始搞起了自己的热修复框架,最开始的像腾讯的Tinker,阿里的AndFix、Sophix,美团的Robust,想要自己实现一套热修复的框架,就需要了解其中的原理
什么是热修复,就是对线上版本的静默更新。当发布线上之后,如果出现了严重的bug,通常需要重新发版来修复,但是频繁地发版显然不是最佳解决方案,正常的发版流程是2个月发布一个新的版本,那么如果在用户无感知的情况下,修复线上bug,避免频繁地发版,热修复就随之出现了。
在开发端,通过Gradle插件生成补丁包,并上传到云端,客户端通过判断是否需要下载新的补丁包,并执行热修复
1 ClassLoader类加载机制
2 Dex动态加载技术 – hook反射
3 差分打包技术 – bsdiff
4 字节码插桩 – ASM Javassist
5 Gradle插件 – 发布差分包
6 so库的编译
目前主流的热修复框架通常采用以下3种修复方式
1 native层hook Java层代码 bug fix
2 编译打包时字节码插桩
3 动态加载Dex文件,类加载技术
AndFix是通过native层hook java层的代码,通常在native层实现热修复是不需要重启修复,这是即时生效的;
例如方法B中有bug,那么需要通过热修复替代这个方法;我们知道,所有方法的调用,都会在JVM中入栈,执行完成之后 出栈,方法在JVM中是一个ArtMethod结构体,那么在JVM运行这个方法之前,在Native层完成这个方法的替换,那么就完成了热修复的工作,而且是即时生效的
Robust采用的技术是编译时字节码插桩技术,这个过程在gradle-plugin中发生,在编译打包阶段,对每个函数注入一段逻辑代码,通过判断是否执行插入的这段代码,这个过程也是即时生效的;
Tinker采用的是Dex动态加载技术,通过反射的方式,将待修复的类放在dexElements数组的前面,在类加载的时候,首先加载这个待修复的类,因为类加载机制不会重复加载类,达到修复的目的,但这个方式是需要重启生效的(出现bug的类在ClassLoader中是不能替换的,存在缓存中,只能重启重新进行类加载)
以上3种方式是目前热修复常见的3种方式,其实各有利弊,像native层处理需要大量的开发成本,跟Robust一样,只能达到修复bug的目的,不能新增类和轻量级的功能;而Tinker则是需要重启才能生效
Android应用和Java类的加载机制基本一致,Java类将代码编译成class文件,JVM加载class文件;而Android多出的一步就是将class文件转换为dex文件,通过dalvik或者Art虚拟机加载,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}"
}
}
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; }
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,这里不要认为父类就是父类加载器,这是两个概念
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) {
······
// save dexPath for BaseDexClassLoader
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; /* * 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(); DexFile dex = null; if (name.endsWith(DEX_SUFFIX)) { //如果是以 .dex为结尾 // Raw dex file (not inside a zip/jar). 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); } } //把剩余的文件拷贝到elements数组中 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类
先看一下两者构造方法的异同
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优化
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。