Android源碼來自28.0.2
ClassLoader
參考Android工程師進(jìn)階 34講
1.每個(gè)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
//首先,先判斷自己是否曾經(jīng)加載過這個(gè)類,
//如果曾經(jīng)加載過,直接返回之前加載的Class
Class<?> c = findLoadedClass(name);
if (c == null) {
//如果沒有加載過,就開始加載
try {
if (parent != null) {
//如果有parent(ClassLoader)
//把這個(gè)class交給parent去加載
c = parent.loadClass(name, false);
} else {
//如果parent為空,說明當(dāng)前classloader是bootstrap class loader
//執(zhí)行findBootstrapClassOrNull方法,
//不過ClassLoader#findBootstrapClassOrNull方法默認(rèn)返回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

從log可以看出,加載MainActivity的是PathClassLoader。
而PathClassLoader繼承自BaseDexClassLoader,BaseDexClassLoader繼承自ClassLoader
, PathClassLoader和BaseDexClassLoader都沒有重寫loadClass方法。PathClassLoader僅僅是重寫了兩個(gè)構(gòu)造方法。
所以PathClassLoader執(zhí)行l(wèi)oadClass的邏輯是:
1.PathClassLoader自己是否曾經(jīng)加載過目標(biāo)class,如果加載過,就直接返回。如果沒加載過,執(zhí)行步驟2
2.執(zhí)行BootClassLoader#loadClass. PathClassLoader的parent是BootClassLoader,不為空,所以交給執(zhí)行BootClassLoader#loadClass(步驟3)
3.在BootClassLoader#loadClass里,
@Override
protected Class<?> loadClass(String className, boolean resolve)
throws ClassNotFoundException {
//同ClassLoader,也是先看自己是否曾經(jīng)親自加載過
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
//如果沒有,就執(zhí)行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。平時(shí)我們調(diào)用Class.forName方法時(shí),最終也是走向了這個(gè)native方法。
到這里,ClassLoader的雙親委托就清楚了,ClassLoader雙親委托邏輯是:先交給parent(注意,這個(gè)parent并不是繼承的那個(gè)父類,而是設(shè)置進(jìn)來的另一個(gè)ClassLoader,單鏈表),如果parent不能加載class,那自己再加載,如果自己也不能加載,就返回null??梢岳斫馐菃捂湵斫M成的一串ClassLoader,每個(gè)ClassLoader里都有一個(gè)parent來指向上一個(gè)ClassLoader,如果一個(gè)ClassLoader的parent是null,那么,這個(gè)就是鏈表頭,每次ClassLoader加載class,它就先找parent,讓parent去加載,parent加載不了(返回null),自己才加載,如果自己也加載不了,就返回null。
如果這里返回null,則會(huì)執(zhí)行PathClassLoader的findClass方法,自己來加載class. PathClassLoader并沒有重寫findClass方法, 但是BaseDexClassLoader重寫了該方法,所以,PathClassLoader會(huì)執(zhí)行BaseDexClassLoader#findClass方法
BaseDexClassLoader#findClass
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
//創(chuàng)建PathClassLoader的時(shí)候就會(huì)初始化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的邏輯實(shí)際上交給了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的邏輯實(shí)際上是由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的邏輯就是遍歷數(shù)組dexElements,而數(shù)組dexElements是在構(gòu)造方法中通過makeDexElements方法生成的。
而這個(gè)dexElements的內(nèi)容,可以打印BaseDexClassLoader#pathList內(nèi)容如下:

pathList的dexElements里只有一個(gè)元素,zip file "/data/app/com.houtrry.hotfixsample-2.apk",也就是當(dāng)前程序的apk文件。
熱修復(fù)
市場上的熱修復(fù)可以分為java層實(shí)現(xiàn)和native層實(shí)現(xiàn)。
native層:andfix sophix(不需要重啟app)
Java層:Tinker(需要重啟app)
本文討論的是Java層實(shí)現(xiàn)。
Java層實(shí)現(xiàn)主要有三種方案
1.在DexPathList的dexElements中插入dex
2.自定義ClassLoader加載dex
3.利用ClassLoader的雙親委托機(jī)制,把PathClassLoader的parent替換成自己的ClassLoader, 在這個(gè)ClassLoader中加載dex
一般來說,方案1和3比較常見
方案 在DexPathList的dexElements中插入dex
原理
DexPathList#findClass通過遍歷數(shù)組dexElements查找class,那么,是否可以把修復(fù)好的class文件放到dexElements中,并且放到dexElements中的apk前面呢?這樣,每次加載目標(biāo)class,都會(huì)先遍歷到處于前面的已經(jīng)修復(fù)了問題的dex,而有問題的dex在apk中,就沒有加載的機(jī)會(huì),從而實(shí)現(xiàn)熱修復(fù)。
- 而怎么把修復(fù)好的dex加到dexElements中呢?通過反射就可以實(shí)現(xiàn)。
- 那什么時(shí)候執(zhí)行這個(gè)邏輯呢?當(dāng)然是越早越好,因?yàn)榧虞d晚了,可能會(huì)出現(xiàn)問題class已被加載的情況,這種情況下,即使dexElements中修復(fù)好的dex位于前面也沒有機(jī)會(huì)執(zhí)行了,只能重啟app后才能生效。app中最早的應(yīng)該就是Application#attachBaseContext方法了,因此,我們?cè)谶@個(gè)方法里執(zhí)行dexElements的插入邏輯。
- dex的來源應(yīng)該是我們下載到本地的,下載完成后,app重啟進(jìn)入Application#attachBaseContext執(zhí)行dexElements的插入邏輯即可生效。
- dex的生成方法可以查看d8使用說明
拿Utils.java舉例,生成dex主要有2步:
①javac Utils.java 生成Utils.class文件
②./d8 Utils.class 生成classes.dex文件,這個(gè)就是想要的dex文件了。
注意:d8文件在\sdk\build-tools下,比如\sdk\build-tools\28.0.2\d8.bat;注意步驟②時(shí)d8的路徑。
實(shí)現(xiàn)
- 在Application#attachBaseContext中,通過反射,在dexElements中插入dex
/**
* 在ClassLoader中的dexElements數(shù)組中(數(shù)組0號(hào)位)插入我們自己的dex
*
* @param application
*/
public static void preformHotFix(@NonNull Application application) {
if (!hasDex(application)) {
return;
}
try {
//第一步:獲取當(dāng)前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)
//當(dāng)然,也可以用另外一種方式: new 一個(gè)PathClassLoader加載dex文件夾下的dex,
// 然后反射獲取到這個(gè)PathClassLoader中dexElements的值,
// 也就是我們這里需要的,反射邏輯可以參考第一步
//PathClassLoader pathClassLoader = new PathClassLoader(getDexPath(getDexDir(application)), classLoaderParent);
//注意:makeDexElements在不同版本中可能會(huì)有變化,注意log提示,做好兼容, 這里只是測(cè)試.
// 具體的兼容邏輯可以參考騰訊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;
}
//第三步:合并兩個(gè)dexElements
//注意:dexElementsNew中的元素需要放到dexElementsOld元素的前面
//數(shù)組拷貝邏輯可以參考DexPathList#addDexPath方法
// Object[] dexElements = new Object[sizeOfNewDexElement + sizeOfOldDexElement];
//注意:這里不要像直接像上面那樣直接new Object[]數(shù)組,而是使用Array.newInstance方法(參考自tinker的com.tencent.tinker.loader.shareutil.ShareReflectUtil#expandFieldArray)
//直接new Object[]數(shù)組的話,在執(zhí)行下面dexElementsField.set的時(shí)候會(huì)報(bào)錯(cuò)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可能會(huì)稍有不同(主要是參數(shù)不同),因此,需要考慮兼容
- 獲取dexElements可以不通過反射makeDexElements的方式,通過new PathClassLoader(dexPath, null),把生成dexElements的邏輯交給PathClassLoader,然后反射獲取PathClassLoader中的dexElements即可獲取
- 創(chuàng)建合并后DexElement數(shù)組容器時(shí),如果使用new Object[]的方式
Object[] dexElements = new Object[sizeOfNewDexElement + sizeOfOldDexElement];
會(huì)在執(zhí)行
dexElementsField.set(pathList, dexElements);
替換dexElementsField值的時(shí)候報(bào)錯(cuò),異常信息如下
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
原理
利用雙親委托機(jī)制,給當(dāng)前parent換成可以加載自己dex的ClassLoader。
原本的ClassLoader路徑:PathClassLoader==>BootClassLoader
替換后的ClassLoader路徑:PathClassLoader==>自定義ClassLoader==>BootClassLoader
實(shí)現(xiàn)
/**
* 給ClassLoader安排一個(gè)新的parent
* 在這個(gè)新parent中加載我們自己的dex
* 簡單理解就是在單鏈表中間插入第三個(gè)元素
*
* @param application
*/
public static void preformHotFix2(@NonNull Application application) {
if (!hasDex(application)) {
return;
}
try {
ClassLoader classLoader = application.getClassLoader();
//第一步:反射獲取到當(dāng)前ClassLoader的parent
Class<?> clsBaseDexClassLoader = Class.forName("java.lang.ClassLoader");
Field parent = clsBaseDexClassLoader.getDeclaredField("parent");
parent.setAccessible(true);
//第二步:創(chuàng)建新的PathClassLoader
// 這個(gè)PathClassLoader的parent是當(dāng)前CLassLoader的parent
//path指向我們dex文件夾下的dex文件
ClassLoader classLoaderParent = classLoader.getParent();
PathClassLoader pathClassLoader = new PathClassLoader(getDexPath(getDexDir(application)), classLoaderParent);
//第三步:把classLoaderParent作為當(dāng)前classLoader的parent
//這樣,根據(jù)雙親委托機(jī)制,當(dāng)前ClassLoader加載class的時(shí)候,
// 會(huì)將class交給它的parent(也就是我們創(chuàng)建的pathClassLoader來加載)
//如果我們的pathClassLoader可以加載這個(gè)class(意味著該class能在dex中找到,也就是我們需要修復(fù)的class)
//這樣系統(tǒng)的ClassLoader就沒有機(jī)會(huì)加載有問題的class,問題得到修復(fù)
parent.set(classLoader, pathClassLoader);
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
這里,我們創(chuàng)建PathClassLoader作為自定義parent。
PathClassLoader的第一個(gè)參數(shù):字符串,dex路徑。文件可以是dex/apk/zip。多個(gè)文件字符串之間用“:”分號(hào)隔開
PathClassLoader的第二個(gè)參數(shù):ClassLoader,也就是指定ClassLoader的parent。這里我們用默認(rèn)ClassLoader的parent作為自定義ClassLoader的parent。
執(zhí)行后日志如下

可以看到,當(dāng)前ClassLoader還是原來的PathClassLoader,加載的dex是base.apk。
parent是我們自定義的ClassLoader,其dex正是我們期望的hotFixCopy.dex。
parent的parent是BootClassLoader,也正是沒改之前的PathClassLoader的parent。
demo地址HotFixSample
tinker源碼分析
//TODO 待完善