Android源码来自28.0.2
ClassLoader
参考Android工程师进阶 34讲
1.每个ClassLoader加载的Class路径不同,
2.ClassLoader加载class主要是通过loadClass方法
ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//首先,先判断自己是否曾经加载过这个类,
//如果曾经加载过,直接返回之前加载的Class
Class<?> c = findLoadedClass(name);
if (c == null) {
//如果没有加载过,就开始加载
try {
if (parent != null) {
//如果有parent(ClassLoader)
//把这个class交给parent去加载
c = parent.loadClass(name, false);
} else {
//如果parent为空,说明当前classloader是bootstrap class loader
//执行findBootstrapClassOrNull方法,
//不过ClassLoader#findBootstrapClassOrNull方法默认返回null
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.
// 如果parent为空或者parent加载不了class
//那就自己加载
c = findClass(name);
}
}
return c;
}
PathClassLoader
image.png从log可以看出,加载MainActivity的是PathClassLoader。
而PathClassLoader继承自BaseDexClassLoader,BaseDexClassLoader继承自ClassLoader
, PathClassLoader和BaseDexClassLoader都没有重写loadClass方法。PathClassLoader仅仅是重写了两个构造方法。
所以PathClassLoader执行loadClass的逻辑是:
1.PathClassLoader自己是否曾经加载过目标class,如果加载过,就直接返回。如果没加载过,执行步骤2
2.执行BootClassLoader#loadClass. PathClassLoader的parent是BootClassLoader,不为空,所以交给执行BootClassLoader#loadClass(步骤3)
3.在BootClassLoader#loadClass里,
@Override
protected Class<?> loadClass(String className, boolean resolve)
throws ClassNotFoundException {
//同ClassLoader,也是先看自己是否曾经亲自加载过
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
//如果没有,就执行Class.classForName去加载
//而Class.classForName是native方法
clazz = findClass(className);
}
return clazz;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return Class.classForName(name, false, null);
}
BootClassLoader#loadClass最后是使用java.lang.Class#classForName加载该class。平时我们调用Class.forName方法时,最终也是走向了这个native方法。
到这里,ClassLoader的双亲委托就清楚了,ClassLoader双亲委托逻辑是:先交给parent(注意,这个parent并不是继承的那个父类,而是设置进来的另一个ClassLoader,单链表),如果parent不能加载class,那自己再加载,如果自己也不能加载,就返回null。可以理解是单链表组成的一串ClassLoader,每个ClassLoader里都有一个parent来指向上一个ClassLoader,如果一个ClassLoader的parent是null,那么,这个就是链表头,每次ClassLoader加载class,它就先找parent,让parent去加载,parent加载不了(返回null),自己才加载,如果自己也加载不了,就返回null。
如果这里返回null,则会执行PathClassLoader的findClass方法,自己来加载class. PathClassLoader并没有重写findClass方法, 但是BaseDexClassLoader重写了该方法,所以,PathClassLoader会执行BaseDexClassLoader#findClass方法
BaseDexClassLoader#findClass
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
//创建PathClassLoader的时候就会初始化pathList
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
if (reporter != null) {
reporter.report(this.pathList.getDexPaths());
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//查找class的逻辑实际上交给了pathList
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的逻辑实际上是由pathList完成的。
DexPathList
private Element[] dexElements;
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
//---------------------省略
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
//---------------------省略
}
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;
}
可以看到DexPathList#findClass的逻辑就是遍历数组dexElements,而数组dexElements是在构造方法中通过makeDexElements方法生成的。
而这个dexElements的内容,可以打印BaseDexClassLoader#pathList内容如下:
image.png
pathList的dexElements里只有一个元素,zip file "/data/app/com.houtrry.hotfixsample-2.apk",也就是当前程序的apk文件。
热修复
市场上的热修复可以分为java层实现和native层实现。
native层:andfix sophix(不需要重启app)
Java层:Tinker(需要重启app)
本文讨论的是Java层实现。
Java层实现主要有三种方案
1.在DexPathList的dexElements中插入dex
2.自定义ClassLoader加载dex
3.利用ClassLoader的双亲委托机制,把PathClassLoader的parent替换成自己的ClassLoader, 在这个ClassLoader中加载dex
一般来说,方案1和3比较常见
方案 在DexPathList的dexElements中插入dex
原理
DexPathList#findClass通过遍历数组dexElements查找class,那么,是否可以把修复好的class文件放到dexElements中,并且放到dexElements中的apk前面呢?这样,每次加载目标class,都会先遍历到处于前面的已经修复了问题的dex,而有问题的dex在apk中,就没有加载的机会,从而实现热修复。
- 而怎么把修复好的dex加到dexElements中呢?通过反射就可以实现。
- 那什么时候执行这个逻辑呢?当然是越早越好,因为加载晚了,可能会出现问题class已被加载的情况,这种情况下,即使dexElements中修复好的dex位于前面也没有机会执行了,只能重启app后才能生效。app中最早的应该就是Application#attachBaseContext方法了,因此,我们在这个方法里执行dexElements的插入逻辑。
- dex的来源应该是我们下载到本地的,下载完成后,app重启进入Application#attachBaseContext执行dexElements的插入逻辑即可生效。
- dex的生成方法可以查看d8使用说明
拿Utils.java举例,生成dex主要有2步:
①javac Utils.java 生成Utils.class文件
②./d8 Utils.class 生成classes.dex文件,这个就是想要的dex文件了。
注意:d8文件在\sdk\build-tools下,比如\sdk\build-tools\28.0.2\d8.bat;注意步骤②时d8的路径。
实现
- 在Application#attachBaseContext中,通过反射,在dexElements中插入dex
/**
* 在ClassLoader中的dexElements数组中(数组0号位)插入我们自己的dex
*
* @param application
*/
public static void preformHotFix(@NonNull Application application) {
if (!hasDex(application)) {
return;
}
try {
//第一步:获取当前ClassLoader中的dexElements(dexElementsOld)
ClassLoader classLoader = application.getClassLoader();
Class<?> clsBaseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = clsBaseDexClassLoader.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader);
Class<?> clsDexPathList = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = clsDexPathList.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object[] dexElementsOld = (Object[]) dexElementsField.get(pathList);
System.out.println("dexElementsOld: " + dexElementsOld);
int sizeOfOldDexElement = dexElementsOld.length;
List<File> dexFileList = getDexFileList(getDexDir(application));
Method[] declaredMethods = clsDexPathList.getDeclaredMethods();
for (Method method :
declaredMethods) {
System.out.println("method: " + method);
}
//第二步:生成包含hot fix dex文件的dexElements(dexElementsNew)
//当然,也可以用另外一种方式: new 一个PathClassLoader加载dex文件夹下的dex,
// 然后反射获取到这个PathClassLoader中dexElements的值,
// 也就是我们这里需要的,反射逻辑可以参考第一步
//PathClassLoader pathClassLoader = new PathClassLoader(getDexPath(getDexDir(application)), classLoaderParent);
//注意:makeDexElements在不同版本中可能会有变化,注意log提示,做好兼容, 这里只是测试.
// 具体的兼容逻辑可以参考腾讯tinker的com.tencent.tinker.loader.SystemClassLoaderAdder#installDexes
Method makeDexElementsMethod = clsDexPathList.getDeclaredMethod("makeDexElements",
List.class, File.class, List.class, ClassLoader.class);
makeDexElementsMethod.setAccessible(true);
Object[] dexElementsNew = (Object[]) makeDexElementsMethod.invoke(null, dexFileList, null,
new ArrayList<IOException>(), classLoader);
int sizeOfNewDexElement = dexElementsNew.length;
System.out.println("sizeOfNewDexElement: " + sizeOfNewDexElement + ", sizeOfOldDexElement: " + sizeOfOldDexElement);
if (sizeOfNewDexElement == 0) {
return;
}
//第三步:合并两个dexElements
//注意:dexElementsNew中的元素需要放到dexElementsOld元素的前面
//数组拷贝逻辑可以参考DexPathList#addDexPath方法
// Object[] dexElements = new Object[sizeOfNewDexElement + sizeOfOldDexElement];
//注意:这里不要像直接像上面那样直接new Object[]数组,而是使用Array.newInstance方法(参考自tinker的com.tencent.tinker.loader.shareutil.ShareReflectUtil#expandFieldArray)
//直接new Object[]数组的话,在执行下面dexElementsField.set的时候会报错java.lang.RuntimeException: Unable to instantiate application com.houtrry.hotfixsample.HotFixApplication: java.lang.IllegalArgumentException: field dalvik.system.DexPathList.dexElements has type dalvik.system.DexPathList$Element[], got java.lang.Object[]
Object[] dexElements = (Object[]) Array.newInstance(dexElementsOld.getClass().getComponentType(), sizeOfNewDexElement + sizeOfOldDexElement);
System.arraycopy(dexElementsNew, 0, dexElements, 0, sizeOfNewDexElement);
System.arraycopy(dexElementsOld, 0, dexElements, sizeOfNewDexElement, sizeOfOldDexElement);
System.out.println("dexElements: " + dexElements);
//第四步:替换dexElements
dexElementsField.setAccessible(true);
dexElementsField.set(pathList, dexElements);
System.out.println("pathList: " + pathList);
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
日志中可以看到,DexPathList中有hotFixCopy.dex和base.apk,且hotFixCopy.dex在base.apk前面,也即是期望效果。
注意:
- 不同Android版本中makeDexElements可能会稍有不同(主要是参数不同),因此,需要考虑兼容
- 获取dexElements可以不通过反射makeDexElements的方式,通过new PathClassLoader(dexPath, null),把生成dexElements的逻辑交给PathClassLoader,然后反射获取PathClassLoader中的dexElements即可获取
- 创建合并后DexElement数组容器时,如果使用new Object[]的方式
Object[] dexElements = new Object[sizeOfNewDexElement + sizeOfOldDexElement];
会在执行
dexElementsField.set(pathList, dexElements);
替换dexElementsField值的时候报错,异常信息如下
2020-05-03 19:51:02.099 3507-3507/com.houtrry.hotfixsample E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.houtrry.hotfixsample, PID: 3507
java.lang.RuntimeException: Unable to instantiate application com.houtrry.hotfixsample.HotFixApplication: java.lang.IllegalArgumentException: field dalvik.system.DexPathList.dexElements has type dalvik.system.DexPathList$Element[], got java.lang.Object[]
at android.app.LoadedApk.makeApplication(LoadedApk.java:802)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5377)
at android.app.ActivityThread.-wrap2(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1545)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
Caused by: java.lang.IllegalArgumentException: field dalvik.system.DexPathList.dexElements has type dalvik.system.DexPathList$Element[], got java.lang.Object[]
at java.lang.reflect.Field.set(Native Method)
at com.houtrry.hotfixsample.HotFixManager.preformHotFix(HotFixManager.java:97)
at com.houtrry.hotfixsample.HotFixApplication.attachBaseContext(HotFixApplication.java:17)
at android.app.Application.attach(Application.java:189)
at android.app.Instrumentation.newApplication(Instrumentation.java:1008)
at android.app.Instrumentation.newApplication(Instrumentation.java:992)
at android.app.LoadedApk.makeApplication(LoadedApk.java:796)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5377)
at android.app.ActivityThread.-wrap2(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1545)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
方案 把PathClassLoader的parent替换成自己的ClassLoader
原理
利用双亲委托机制,给当前parent换成可以加载自己dex的ClassLoader。
原本的ClassLoader路径:PathClassLoader==>BootClassLoader
替换后的ClassLoader路径:PathClassLoader==>自定义ClassLoader==>BootClassLoader
实现
/**
* 给ClassLoader安排一个新的parent
* 在这个新parent中加载我们自己的dex
* 简单理解就是在单链表中间插入第三个元素
*
* @param application
*/
public static void preformHotFix2(@NonNull Application application) {
if (!hasDex(application)) {
return;
}
try {
ClassLoader classLoader = application.getClassLoader();
//第一步:反射获取到当前ClassLoader的parent
Class<?> clsBaseDexClassLoader = Class.forName("java.lang.ClassLoader");
Field parent = clsBaseDexClassLoader.getDeclaredField("parent");
parent.setAccessible(true);
//第二步:创建新的PathClassLoader
// 这个PathClassLoader的parent是当前CLassLoader的parent
//path指向我们dex文件夹下的dex文件
ClassLoader classLoaderParent = classLoader.getParent();
PathClassLoader pathClassLoader = new PathClassLoader(getDexPath(getDexDir(application)), classLoaderParent);
//第三步:把classLoaderParent作为当前classLoader的parent
//这样,根据双亲委托机制,当前ClassLoader加载class的时候,
// 会将class交给它的parent(也就是我们创建的pathClassLoader来加载)
//如果我们的pathClassLoader可以加载这个class(意味着该class能在dex中找到,也就是我们需要修复的class)
//这样系统的ClassLoader就没有机会加载有问题的class,问题得到修复
parent.set(classLoader, pathClassLoader);
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
这里,我们创建PathClassLoader作为自定义parent。
PathClassLoader的第一个参数:字符串,dex路径。文件可以是dex/apk/zip。多个文件字符串之间用“:”分号隔开
PathClassLoader的第二个参数:ClassLoader,也就是指定ClassLoader的parent。这里我们用默认ClassLoader的parent作为自定义ClassLoader的parent。
执行后日志如下
可以看到,当前ClassLoader还是原来的PathClassLoader,加载的dex是base.apk。
parent是我们自定义的ClassLoader,其dex正是我们期望的hotFixCopy.dex。
parent的parent是BootClassLoader,也正是没改之前的PathClassLoader的parent。
demo地址HotFixSample
tinker源码分析
//TODO 待完善
网友评论