通过Hook类的加载器,将我们的dex插入到dex元素数组的最前面达到热修复的目的,通常情况下类只会被加载一次
随着公司的业务越来越复杂,代码迭代次数过多导致代码难以维护,很多潜在的逻辑关联容易被忽略,虽然在发版的时候有做灰度和ab,还是难以避免出现一些奇奇怪怪的bug或者机型适配问题。所以当务之急是接入热修复。本篇内容主要分析热修复原理,在文章最后会有一个demo对全文做一个概括。希望能对你有一些帮助
经测试发现android 10上可以完美运行
ClassLoader是一个抽象类,用于类的加载操作。
try {
ClassLoader classLoader = getClass().getClassLoader();
Class<?> aClass = classLoader.loadClass("类的全路径");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
看一下loadClass(String name)的具体实现
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) {
// 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;
}
这里出现了一个非常典型的双亲委托例子:
通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
为什么要这么多,双亲委托有什么好处呢?
继续分析源码:
findBootstrapClassOrNull(name);是一个空实现,该方法直接返回null,所以实际上加载类的操作落到了findClass(String name)。
我们去查看findClass的代码,居然发现下面这种情况
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
前面我们提到ClassLoader只是一个抽象类,我们在调用getClass().getClassLoader()方法返回的ClassLoader实际上是一个PathClassLoader,而PathClassLoader继承与BaseDexClassLoader
继承关系如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ct7cel5m-1593345583256)(/Users/a1/Library/Application Support/typora-user-images/image-20200628145613651.png)]
所以这时候我们应该去PathClassLoader中去找findClass(String name)方法,但但遗憾的是没有找到,所以转而去BaseDexClassLoader中去寻找,Bingo!
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// First, check whether the class is present in our shared libraries.
if (sharedLibraryLoaders != null) {
for (ClassLoader loader : sharedLibraryLoaders) {
try {
return loader.loadClass(name);
} catch (ClassNotFoundException ignored) {
}
}
}
// Check whether the class in question is present in the dexPath that
// this classloader operates on.
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;
}
我们看到Class c = pathList.findClass(name, suppressedExceptions)这句代码,这是加载Class的最后一步,如果这里还找不到就要抛出ClassNotFoundException了。pathList是DexPathList的引用,那么DexPathList又是什么呢?看一下该类的注释:
/**
* A pair of lists of entries, associated with a {@code ClassLoader}.
* One of the lists is a dex/resource path — typically referred
* to as a "class path" — list, and the other names directories
* containing native code libraries. Class path entries may be any of:
* a {@code .jar} or {@code .zip} file containing an optional
* top-level {@code classes.dex} file as well as arbitrary resources,
* or a plain {@code .dex} file (with no possibility of associated
* resources).
*
* This class also contains methods to use these lists to look up
* classes and resources.
*
* @hide
*/
public final class DexPathList {
...
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
@UnsupportedAppUsage
private Element[] dexElements;
//构造方法
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
//。。。省略一大堆检查操作
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// 解析出dexElements
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
//。。。。省略一些native操作
}
}
DexPathList的构造方法里面包含了以下几个参数
makeDexElements方法将给定路径下的.dex(也可以是.apk, .jar)解析为Element数组并赋值给dexElements。
DexPathList的创建是在BaseDexClassLoader的构造方法里面。
我们再反过头来看Class c = pathList.findClass(name, suppressedExceptions);
public Class<?> findClass(String name, List<Throwable> suppressed) {
//通过遍历dexElements来查找类
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;
}
追根溯源,到最后才发现一切的根源都是dexElements。如果我们能对这个对象进行一些操作,那么热修复也就变得可能。
到这里可能有点蒙,我们理一下这个流程
整体的思路如下:
baseDexClassLoader.pathList.dexElements = “我们创建的新Element数组”
通过javac将java文件转为.class
在sdk目录下的build_tools文件夹下选择相应的版本,找到d8这个文件。命令行中执行
//将多个class打包进dex
./d8 1.class 2.class 3.class
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q2a5oQZJ-1593345583257)(/Users/a1/Library/Application Support/typora-user-images/image-20200628162801996.png)]
执行完成后会在d8同目录下生成classes.dex
前面已经分析了过程,话不多说,直接上代码了
public void hotFix(Context context) {
try {
//dex文件路径
String dexPath = context.getCacheDir() + File.separator + "hotfix.dex";
File file = new File(dexPath);
if (!file.exists()) {
return;
}
//获取类的加载器
ClassLoader classLoader = getClass().getClassLoader();
Class<BaseDexClassLoader> loaderClass = BaseDexClassLoader.class;
//获取BaseDexClassLoader的pathList属性
Field pathListFiled = loaderClass.getDeclaredField("pathList");
pathListFiled.setAccessible(true);
Object dexPathListObject = pathListFiled.get(classLoader);
Class<?> dexPathListClass = dexPathListObject.getClass();
//通过pathList获取到dexElements属性
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object oldElementObject = dexElementsField.get(dexPathListObject);
//构建出一个全新的pathClassLoader,加载的是本地dex
PathClassLoader newPathClassLoader = new PathClassLoader(dexPath, null);
Object newPathListObject = pathListFiled.get(newPathClassLoader);
Object newElementsObject = dexElementsField.get(newPathListObject);
//将新老数组融合在一起组成一个新的Element数组并赋值给DexPathList的dexElements属性
int oldLength = Array.getLength(oldElementObject);
int newLength = Array.getLength(newElementsObject);
Object concatElementsObject = Array.newInstance(oldElementObject.getClass().getComponentType(), oldLength + newLength);
for (int i = 0; i < newLength; i++) {
Array.set(concatElementsObject, i, Array.get(newElementsObject, i));
}
for (int i = 0; i < oldLength; i++) {
Array.set(concatElementsObject, i + newLength, Array.get(oldElementObject, i));
}
//将新的element[]赋值给dexElementsField
dexElementsField.set(dexPathListObject, concatElementsObject);
} catch (Exception e) {
e.printStackTrace();
}
}
加载补丁的操作可以放到attachBaseContext中进行,该方法的调用时机甚至早于onCreate
这里只是提到了对于类文件的替换,如果修改了资源文件又该如何呢?请参考
https://blog.csdn.net/u013894711/article/details/105166872