H5W3
当前位置:H5W3 > JavaScript > 正文

app热修复原理

热修复:不用安装,静默修复。

正常情况下:版本1.0上线,用户安装,发现bug,紧急修复,重新发布1.1版本,用户手动安装(全量更新)。
热修复: 版本1.0上线,用户安装,发现bug,紧急修复,打出补丁推送给用户,自动拉取补丁修复。

热修复优势

1、对开发者:无需重新发布新版本,省时省力。
2、用户:无感知修复,也无需下载更新应用,代价小。
3、App:修复成功率高,把损失降到最低。

热修复能完成什么修复

1、代码修复
2、资源修复
3、so库修复

热修复分类

热修复原理详解| 掘金技术征文
热修复原理详解| 掘金技术征文

Android四层架构为:1. 应用程序层 Application 2. 应用程序框架层 Application Framework 3. 系统运行库层 Libaries
4. Linux核心层

上图java属于 Application Framework 层,native hook属于Libaries层。

热修复插桩原理

先看源码

类加载器

(apk在安卓手机安卓以后,会复制apk到data/app/pacageName~1/base.apk,因为下载apk的路径一般是sd卡,不安全,容易被删除)
apk进行解压,会有如下的包:
patch.dex、classes.dex 、classes1.dex、classes2.dex、classes3.dex、···.dex
它们都会在dexElement[]数组里面

在jvm中,java是通过ClassLoader来加载应用中的class的,而Android对jvm优化过,使用的是dalvik,android是用DexClassLoader或者 PathClassLoader (他们是ClassLoader的子类)来加载dex文件中的class文件的。
DexClassLoader类会加载classes.dex文件,那么加载类,怎么寻找呢?

BaseDexClassLoader源码
public class BaseDexClassLoader extends ClassLoader {
// 需要加载的dex列表
private final DexPathList pathList;
// dexPath要加载的dex文件所在的路径,optimizedDirectory是odex将dexPath
// 处dex优化后输出到的路径,这个路径必须是手机内部路劲,libraryPath是需要
// 加载的C/C++库路径,parent是父类加载器对象
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>();
// 使用pathList对象查找name类
Class c = pathList.findClass(name, suppressedExceptions);
return c;
}
}
复制代码
Element(Element类代表dex文件或资源文件的路径元素)
/*package*/ static class Element {
private final File file;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
private ZipFile zipFile;
private boolean initialized;
// file文件,是否是目录,zip文件通常都是apk或jar文件,dexFile就是.dex文件
public Element(File file, boolean isDirectory, File zip, DexFile dexFile) {
this.file = file;
this.isDirectory = isDirectory;
this.zip = zip;
this.dexFile = dexFile;
}
}
复制代码
DexPathList源码
/*package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private final ClassLoader definingContext;
//
private final Element[] dexElements;
// 本地库目录
private final File[] nativeLibraryDirectories;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
// 当前类加载器的父类加载器
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// 根据输入的dexPath创建dex元素对象
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
}
复制代码

makeDexElements就是把前面dexPath里面解析到的路径下的文件全部遍历一遍,如果是dex文件或apk和jar文件就会查找它们内部的dex文件,将所有这些dex文件都加入到Element数组中,完成加载路径下面的所有dex解析。

makeDexElements方法
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
// 所有从dexPath找到的文件
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
// 如果是文件夹,就直接将路径添加到Element中
if (file.isDirectory()) {
elements.add(new Element(file, true, null, null));
} else if (file.isFile()){
// 如果是文件且文件名以.dex结束
if (name.endsWith(DEX_SUFFIX)) {
try {
// 直接从.dex文件生成DexFile对象
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else {
zip = file;
try {
// 从APK/JAR文件中读取dex文件
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
复制代码

在findClass方法里查找名称为name的类时只需要遍历Element数组找是dexFile就直接调用DexFile.loadClassBinaryName方法,这个方法能够从dex文件数据中生成Class对象。

// 加载名字为name的class对象
public Class findClass(String name, List<Throwable> suppressed) {
// 遍历从dexPath查询到的dex和资源Element
for (Element element : dexElements) {
DexFile dex = element.dexFile;
// 如果当前的Element是dex文件元素
if (dex != null) {
// 使用DexFile.loadClassBinaryName加载类
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
复制代码

总结:BaseDexClassLoader 会加载一个DexPathList类型的pathList,pathList是一个dex列表,而pathList是Element[]类型的dexElements转化成的list,dexElements数组里面是所有dex文件,所以pathList也是数组里面是所有dex文件,当程序需要加载类的时候会直接遍历所有的Element对象,查找到和dex文件相关的Element就直接加载数据生成Class对象。

热修复思路

热修复原理详解| 掘金技术征文

上面源码得出,BaseDexClassLoader 加载pathList,pathList对象里面有Element[]数组,Element[]从前往后依次加载classesX.dex文件,当出现bug的时候,将补丁classesX.dex文件放到Element[]最前面,这样就会加载补丁dex文件,不再加载有bug的dex文件。(因为apk是压缩文件,文件与文件之间是有关系的,如果直接删掉bug的dex文件,是会出问题的,所以直接把文件添加到最前面即可)
那么,如何讲一个dex文件,加到Element[]里面呢?
答:可以新建一个BaseDexClassLoader类加载器,BaseDexClassLoader会先把补丁.dex文件加载出来,BaseDexClassLoader也有一个pathList对象,pathList里面也有Elements[补丁.dex]数组,这个时候可以把带有补丁的Elements[补丁.dex]复制到原程序的Element[]数组中即可。

那么,Elements[补丁.dex]如何复制到原程序的Element[]呢?
答:用过反射。
反射两个BaseDexClassLoader类拿到他们的Elements的值(getField()方法),两个Elements值拼在一起(数组拼接,常规方法,很简单)


复制代码

思路:
从服务器下载修复好的dex包,下载到本机,替换原有的有bug的dex文件
前提是有classes.dex(主包)、classes.dex(bug包),主包不能有bug;必须是运行的时候

什么场景用热修复、插件化、增量更新

热修复:修复线上的bug
插件化:功能的拓展
增量更新:差分的补丁

本文地址:H5W3 » app热修复原理

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址