写在前面:
参考文章 热修复——深入浅出原理与实现
一、简述和意义
在热修复之前,一个上线的app如果出现了bug,即使非常小,要是想及时更新就必须将app重新打包发布到应用市场,让用户重新下载安装,使得用户体验非常差,而且很多用户不愿意去经常更新app,所以严重的bug还会造成用户流失,甚至带来严重的后果。
热修复技术就是能在用户不用下载安装新的app,甚至无感知的情况下修复一些紧急或者必须的bug的技术。该技术是这几年比较火的技术,也是项目非常需要的技术,更是作为开发者必须学习的技能之一。
目前比较火的热修复方案分为两派,分别是:
- 阿里系 Hotfix Sophix 从底层二进制入手
- 腾讯系 Tinker 从java加载机制入手
备注:本篇就是基于java加载机制,来研究热修复的原理和实现
二、Dex分包方案分析
2.1 分包方案由来(Dalvik限制)
当一个app功能越来越复杂,可能就会出现编译失败,因为一个jvm中存储方法个数id用的是short类型,导致dex中方法数不能超过65535
基于此,就需要对app编译的时候进行分包,即将编译好的class文件拆分打包成多个dex,绕过dex方法数量的限制以及安装时的检查,在运行时在动态加载其他的dex文件,即其他dex文件在Application初始化的时候被注入到系统的ClassLoader中。
2.2 Dex分包加载的原理(ClassLoader源码分析)
在Android中,要加载dex中的class文件就需要用到PathClassLoder和DexClassLoder这两个专用的类加载器,它们都继承于BaseDexClassLoader
以下是Android5.0中的部分源码
PathClassLoader.java
DexClassLoader.java
BaseDexClassLoader.java
DexPathList.java
使用场景
- PathClassLoader:只能加载已经安装到Android系统中的apk文件(data/app目录),是Android默认的类加载器
- DexClassLoader:可以加载任意目录下的dex/jar/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);
}
}
- 参数解释:
- dexPath:要加载的patch包文件(一般是dex文件,也可以是jar/zip/apk文件)的文件目录
- optimizedDierctory:dex文件的输出目录(因为在加载jar/apk/zip等压缩格式的文件是会解压出其中的dex文件,该目录就是用于存放这些被解压出来的dex文件的)
- libreayPath:要加载patch文件时需要用到的库路径
- parent:父类加载器
通过上面的比对,可以得出2个结论:
- PathClassLoader和DexClassLoader都继承于BaseDexClassLoader
- PathClassLoader与DexClassLoader在构造方法中都是调用的父类的构造方法,但是DexClassLoader多传了一个optimizedDirectory
所以我们重点还是看BaseDexClassLoader都做了什么
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
...
//查找要加载的class
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 实质是通过pathList的对象findClass()方法来获取class
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;
}
}
类加载器会提供一个方法供外界找到它所加载的class,这个方法就是上面的findClass(),可以看到findClass()内部是通过构造方法中创建的对象DexPathList的findClass()来获取对应的Class的,接下来分析DexPathList的源码
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath),
optimizedDirectory,suppressedExceptions);
...
}
上面构造方法中,调用了makeDexElements方法得到了Element[]数组,这个数组就是通过将一个个patch包封装成一个个Element对象后得到的集合,对于内部调用的splitDexPath(dexPath)是将传进来的dexPath按照(” : “)分割成的一个List集合,所以可以得知dexPath是一个以(” : “)分割的多个dex文件目录拼成的字符串,这个方法比较简单,代码就不贴出来了,下面makeDexElements的源码加注释:
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文件,loadDexFile()是加载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()]);
}
最后再看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;
}
从上面可以看出DexPathList的findClass非常简单,就是对Element[]数组进行遍历,一但找到与name相同的类时,就直接返回找到的class,找不到则返回null
三、基于类加载器的热修复的实现原理
经过上面对PathClassLoader、DexClassLoader、BaseDexClassLoader、DexPathList的分析,我们知道,Android的类加载器在加载一个类的时候会从DexPathList中的Element[]数组进行遍历,找到对应的DexFile,使用DexFile的loadClassBinaryName去加载对应的类。
所以,我们只需要让已经修复好的class打包成一个dex文件,使用DexClassLoader去加载dex文件,得到对应的Element数组,放在应用原Element数组的最前面,这样就能保证获取到的Class是最新修复好的class了,(其实有bug的Class也是存在的,只是没有机会被加载到而已)
实现方式是:创建对象DexClassLoader去加载补丁,加载后反射获取得到的pathList和dexElements数组,将补丁dexElements数组与App原来的dexElements数组进行合并,然后将这个新的Element数组通过反射的方式赋值给当前类加载器的pathList的属性dexElements,这样在加载类的时候就保证先加载到修复好bug的Class了
四、热修复的简单实现
经过上面那么多的理论分析,是时候实践一下了
1. 得到dex补丁
- 修复好有问题的java文件
将java文件编译成class文件(Android studio Build->Rebuild Project,完成后从build目录找到对应的class文件)
将修复好的class文件复制出来,注意在复制该class文件时,需要将它所在的完整包目录一起复制,对应于上图,复制出来的目录结构应该是:
将class文件打包成dex文件
要想将class文件打包成dex文件,需要用到dx命令,这个命令类似于java命令,我们知道,java命令有javac、jar等等,之所以可以使用这类命令,是因为我们有jdk,而dx命令是由Android SDK提供的,它在build_tools目录下的各个Android版本目录中
要想使用dx指定,有两种方式- 配置环境变量(添加到classpath),然后终端在任意位置都能使用
- 不配置环境变量,只能在dx所在目录使用
dx –dex –output=dex/classes2.dex dex
dx –dex –output=(dex输出目录) 空格 (要打包的完整class所在目录)
2. 加载dex格式补丁
根据原理,以下为加载patch包的简单工具类
public class FileDexUtils {
private static final String DEX_SUFFIX = ".dex";
private static final String APK_SUFFIX = ".apk";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
public static final String DEX_DIR = "odex";
private static final String OPTIMIZE_DEX_DIR = "optimize_dex";
private static FileDexUtils INSTANCE;
private FileDexUtils() {
}
public static FileDexUtils getInstance() {
if (INSTANCE == null) {
synchronized (FileDexUtils.class) {
if (INSTANCE == null) {
INSTANCE = new FileDexUtils();
}
}
}
return INSTANCE;
}
/**
* 根据指定目录将指定目录下的所有符合patch包规则的文件 的文件目录以:分割拼接成字符串
*
* @param context
* @param patchFileDir
*/
public void loadFixedDex(Context context, File patchFileDir) {
if (context == null) {
return;
}
StringBuilder loadedDex = new StringBuilder();
File fileDir = patchFileDir != null ? patchFileDir : new File(context.getFilesDir(), DEX_DIR);
File[] listFiles = fileDir.listFiles();
if (listFiles == null) {
return;
}
for (File file : listFiles) {
if (file.getName().startsWith("classes") &&
(file.getName().endsWith(DEX_SUFFIX)
|| file.getName().endsWith(APK_SUFFIX)
|| file.getName().endsWith(ZIP_SUFFIX)
|| file.getName().endsWith(JAR_SUFFIX))) {
loadedDex.append(file.getAbsolutePath()).append(":");
}
}
if (loadedDex.length() > 0) {
loadedDex.replace(loadedDex.length() - 1, loadedDex.length(), "");
}
doDexInject(context, loadedDex.toString());
}
/**
* 根据拼接好的patch包文件目录去进行加载
* @param context
* @param loadedDex
*/
private void doDexInject(Context context, String loadedDex) {
if (TextUtils.isEmpty(loadedDex)) {
return;
}
String optimizeDir = context.getFilesDir().getAbsolutePath() + File.separator + OPTIMIZE_DEX_DIR;
File fopt = new File(optimizeDir);
if (!fopt.exists()) {
fopt.mkdirs();
}
try {
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
//1 加载指定要修复的dex文件
DexClassLoader dexClassLoader = new DexClassLoader(loadedDex, optimizeDir, null, pathClassLoader);
//2 获取BaseDexClassLoader内部的DexPathList
Object dexPathList = getPathList(dexClassLoader);
Object pathPathList = getPathList(pathClassLoader);
//3 获取DexPathList内部的DexElements
Object leftDexElements = getDexElements(dexPathList);
Object rightDexElements = getDexElements(pathPathList);
//4 将两个DexElements合并
Object dexElements = combineArray(leftDexElements, rightDexElements);
//重新给DexPathList的Element[] 赋值
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
private Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
return getField(pathList, pathList.getClass(), "dexElements");
}
private Object getPathList(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
return getField(classLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 反射得到对象中的属性值
*/
private Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
/**
* 反射给对象的属性重新赋值
*/
private void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj, value);
}
/**
* 数组合并
*
* @param arrayLeft
* @param arrayRight
* @return
*/
private Object combineArray(Object arrayLeft, Object arrayRight) {
Class<?> componentType = arrayLeft.getClass().getComponentType();
int i = Array.getLength(arrayLeft);
int j = Array.getLength(arrayRight);
int k = i + j;
Object result = Array.newInstance(componentType, k);
System.arraycopy(arrayLeft, 0, result, 0, i);
System.arraycopy(arrayRight, 0, result, i, j);
return result;
}
}
在application中加载patch包
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
boolean sdCardExist = Environment.getExternalStorageState()
.equals(android.os.Environment.MEDIA_MOUNTED);
if (sdCardExist) {
FileDexUtils.getInstance().loadFixedDex(this, Environment.getExternalStorageDirectory());
}
}
}
下面就可以写demo按照上面步骤尝试以下bug修复啦
上面只是简单的patch包的加载,是基于类加载机制的热修复的核心原理,但若真想运用到项目中还要考虑patch包的下发和下载,版本的区分,系统类加载器不同版本源码的区分等流程和逻辑的控制,也是比较复杂的一套流程。