一、背景
热修复技术慢慢的成为Android开发必不可少的技术,也是成为一名高级程序员必不可少的技能之一。那么什么是热修复技术呢?
当app上线之后,发现了一个严重的bug,需要紧急修复,按照以往的惯例是修复bug、打包测试、发布到各个应用市场、用户覆盖安装,这样不仅耗时耗力,而且也很影响用户的体验。那么有没有一种技术可以在不升级app版本的情况下,直接就将bug修复呢?热修复技术就应运而生。
目前比较火的热修复方案主要有:
1、阿里系:AndFix ,从native层进行修复
2、腾讯系:代表有Tinker,从Java的类加载机制入手
热修复技术需要解决的问题有类替换,资源替换,dalvik与art虚拟机的兼容,阿里系还需要处理不同版本系统的兼容等。本篇主要介绍热修复技术中的类加载机制与实现
二、类加载机制
在Android中,class文件最终会被优化成dex文件,并通过PathClassLoader和DexClassLoader来加载
接下来分析一下PathClassLoader和DexClassLoader源码文件,来看看Android是如何加载类文件的,涉及到的文件(7.0.0系统): PathClassLoader 、 DexClassLoader 、 BaseDexClassLoader 、 DexPathList
首先看看PathClassLoader和DexClassLoader的源码:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath,new File(optimizedDirectory),librarySearchPath,parent);
}
}
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);
}
}
D exClassLoader和PathClassLoader的代码很简单,都是继承自BaseDexClassLoader,并且构造方法也同样都调用了父类的构造方法,区别就是第二个参数,DexClassLoader有优化目录参数optimizedDirectory,而PathClassLoader的参数为null
也正是因为这点,导致DexClassLoader和PathClassLoader有不同的使用场景:DexClassLoader可以用于加载任意目录下的dex、zip、jar、apk里面的dex文件,而PathClassLoader只能加载应用路径下的目录(/data/data/app目录)
1、构造方法
既然都是继承自BaseDexClassLoader,那么我们看看BaseDexClassLoader的构造方法都做了哪些事:
/**
* Constructs an instance.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; may be {@code null}
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
我们先看看参数:
dexPath:含有类和资源的jar/apk的路径,多个路径默认会被冒号分开
optimizedDirectory:优化路径,放置优化后的dex文件的路径,app安装的时候会将dex文件先优化成odex文件才能被使用,此路径就是用于放置odex文件的路径,并且此路径只能是app的内部路径(data/data/包名/XXX)
librarySearchPath:加载类时需要使用的本地库
parent:父加载器
在构造方法中初始化了DexPathList,我们来看看DexPathList的构造方法:
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
//省略部分代码
//赋值类加载器
this.definingContext = definingContext;
// 将dex文件或压缩包中的信息保存到dexElements中
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext);
//省略部分代码
}
那么dexElements是什么呢?来看看声明的地方:
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private
Element[] dexElements;
从注释可以看出,dexElements实际上是个Element集合,再来看看makeDexElements方法是如何将信息保存到dexElements中的:
/**
* Makes an array of dex/resource path elements, one per element of
* the given array.
*/
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions,ClassLoader loader) {
return makeElements(files,optimizedDirectory,suppressedExceptions,false,loader);
}
是个重载,继续看看makeElements方法:
private static Element[] makeElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions,boolean ignoreDexFiles,ClassLoader loader) {
//实例化dex文件或者包含dex文件的的文件长度的Element数组
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) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
//省略部分代码
//如果是dex文件
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory, loader,elements);
} else {
//如果是那些包含dex文件的压缩文件
zip = file;
dex = loadDexFile(file, optimizedDirectory, loader, elements);
}
}
//省略部分代码
return elements;
}
到这里,基本就可以明白,DexPathList的构造方法是将dex文件或者包含dex文件的压缩文件路径转化为一个个Element对象,然后保存到elements数组中
2、findClass
在PathClassLoader和DexClassLoader中是通过父类的BaseDexClassLoader中的findClass来将类加载进来的,我们看看源码中的实现:
@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;
}
我们可以看到,给findClass方法传入类名,通过我们在BaseDexClassLoader实例化的pathList的findClass方法返回需要加载的类Class,那么我们继续跟进DexPathList:
/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
从中我们可以看出,整体就是循环遍历dexElements,然后通过取出element的dexFile,并通过loadClassBinaryName方法返回所需要的类,不为空就直接返回,到这里,就终于看到了返回的类,这里也是热修复的关键:循环遍历含有dex路径的element,如果能从这个element中返回类,则直接返回,循环终止!
三、热修复的思路
通过上面一长串源码的分析研究,我们就可以得出,app的类加载器通过DexPathList循环遍历elements数组,并从遍历的Element对象中查找所需要的类,如果找到则直接返回。注意,从上面的源码分析来看,这里面的每个Element相当于是一个dex文件,那么假如我们的项目有个class有了bug,我们如果将修复好的class文件编译成dex文件,然后放到elements数组的前面,那么就会直接返回修复好的class,后面含有bug的class所在的element则不会遍历到,达到修复的目的,原理图如下
四、实践
前期准备:
1、由于Android studio的instant Run也使用了热修复的的原理,会对测试造成一定的影响,先关闭Android studio的instant run功能:
2、将修复的类的class文件编译成dex文件
将class文件打包成dex文件需要适用到Android自带的dx.bat工具
使用如下命令
dx --dex --output=生成的补丁的路径 要打包的完整class文件所在路径
dx --dex --output=E:\BlogCode\HotUpdateDemo\app\build\intermediates\classes\debug\patch.dex mfy\com\hotupdatedemo\TestUtil.class
将补丁放到手机里面,我放在了内存卡的根目录下
3、添加分包处理
在gradle里面multiDexEnabled true
自定义Application
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(base);
}
}
加载补丁:
public static void fix(Context context, String patchPath) {
if (context == null) return;
File fileDir = context.getDir(Const.DEX_DIR, Context.MODE_PRIVATE);
//创建优化目录
if (!fileDir.exists()) {
fileDir.mkdirs();
}
doInject(context,
patchPath,//dex补丁所在的路径,可以任意路径
fileDir,//app内部路径,存放优化dex优化文件的路径
Const.PATCH_NAME);
}
private static void doInject(Context appContext, String patchPath, File fileDir, String patchName) {
//获取应用内部的类加载器
PathClassLoader pathClassLoader = (PathClassLoader) appContext.getClassLoader();
//实例化dexClassLoader用于加载补丁dex
DexClassLoader dexClassLoader = new DexClassLoader(patchPath, fileDir.getAbsolutePath(), null, pathClassLoader);
try {
//获取dexclassloader和pathclassloader的dexpathlist
Object dexPathList = getPathList(dexClassLoader);
Object pathPathList = getPathList(pathClassLoader);
//获取补丁的elements数组
Object dexElements = getDexElements(dexPathList);
//获取程序的elements
Object pathElements = getDexElements(pathPathList);
//合并两个数组
Object resultElements = combineArray(dexElements, pathElements);
//将合并后的数组设置给PathClassLoader
setField(pathPathList, pathPathList.getClass(), "dexElements", resultElements);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
private static void setField(Object pathPathList, Class<?> clazz, String fieldName, Object resultElements) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(pathPathList, resultElements);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> clazz = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = Array.getLength(arrayRhs) + i;
Object result = Array.newInstance(clazz, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
//获得dexElements
private static Object getDexElements(Object dexPathList) {
return getField(dexPathList, dexPathList.getClass(), "dexElements");
}
//获得DexPathList
private static Object getPathList(Object classLoader) throws ClassNotFoundException {
return getField(classLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
//通过反射获取一个类私有属性的值
private static Object getField(Object obj, Class<?> clazz, String fieldName) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}