最近在搞android插件化,其中最核心的一點就是如何讓插件apk跑起來,其實就利用動態加載,將插件中的類加載到host中。之前也看過android類的加載機制,感覺對于整個過程大致看懂了,然后就開始整插件化,結果碰到問題了又感覺一知半解的,很是不爽,索性放一放插件化,先把android的類加載機制看個明白。這里給自己總結下,加深印象的同時保留筆記。
首先需要知道的是android中兩個主要的classloader,PathClassLoader和DexClassLoader,它們都繼承自BaseDexClassLoader,這兩個類有什么區別呢?其實看一下它們的源碼注釋就一目了然了。
因為代碼很少,約等于沒有,這里直接貼出它們的源碼,其實主要是注釋:
PathClassLoader
package dalvik.system;
/**
* Provides a simple {@link ClassLoader} implementation that operates on a list
* of files and directories in the local file system, but does not attempt to
* load classes from the network. Android uses this class for its system class
* loader and for its application class loader(s).
*/
public class PathClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code PathClassLoader} that operates on a given list of files
* and directories. This method is equivalent to calling
* {@link #PathClassLoader(String, String, ClassLoader)} with a
* {@code null} value for the second argument (see description there).
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
/**
* Creates a {@code PathClassLoader} that operates on two given
* lists of files and directories. The entries of the first list
* should be one of the following:
*
* <ul>
* <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
* well as arbitrary resources.
* <li>Raw ".dex" files (not inside a zip file).
* </ul>
*
* The entries of the second list should be directories containing
* native library files.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
由注釋看可以發現PathClassLoader被用來加載本地文件系統上的文件或目錄,但不能從網絡上加載,關鍵是它被用來加載系統類和我們的應用程序,這也是為什么它的兩個構造函數中調用父類構造器的時候第二個參數傳null,具體的參數意義請看接下來DexClassLoader的注釋。
DexClassLoader
package dalvik.system;
import java.io.File;
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*
* <p>This class loader requires an application-private, writable directory to
* cache optimized classes. Use {@code Context.getDir(String, int)} to create
* such a directory: <pre> {@code
* File dexOutputDir = context.getDir("dex", 0);
* }</pre>
*
* <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* <p>The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @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; must not be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
DexClassLoader用來加載jar、apk,其實還包括zip文件或者直接加載dex文件,它可以被用來執行未安裝的代碼或者未被應用加載過的代碼。這里也寫出了它需要的四個參數的意思
- dexPath:需要被加載的文件地址,可以多個,用File.pathSeparator分割
- optimizedDirectory:dex文件被加載后會被編譯器優化,優化之后的dex存放路徑,不可以為null。注意,注釋中也提到需要一個應用私有的可寫的一個路徑,以防止應用被注入攻擊,并且給出了例子 File dexOutputDir = context.getDir("dex", 0);
- libraryPath:包含libraries的目錄列表,plugin中有so文件,需要將so拷貝到sd卡上,然后把so所在的目錄當參數傳入,同樣用File.pathSeparator分割,如果沒有則傳null就行了,會自動加上系統so庫的存放目錄
- parent:父類構造器
這里著重看一下第二個參數,之前說過PathClassLoader中調用父類構造器的時候這個參數穿了null,因為加載app應用的時候我們的apk已經被安裝到本地文件系統上了,其內部的dex已經被提取并且執行過優化了,優化之后放在系統目錄/data/dalvik-cache下。
接下來我們看一下類的具體加載過程
首先看一段如何使用類加載器加載的調用代碼:
try {
File file = view.getActivity().getDir("dex",0);
String optimizedDirectory = file.getAbsolutePath();
DexClassLoader loader = new DexClassLoader("需要被加載的dex文件所在的路徑",optimizedDirectory,null,context.getClassLoader());
loader.loadClass("需要加載的類的完全限定名");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
這里我們就用了自定義了一個DexClassLoaderLoader,并且調用了它的loadClass方法,這樣一個需要被使用的類就被我們加載進來了,接下去就可以正常使用這個類了,具體怎么使用我就不多說了,我們還是來研究研究這個類是怎么被加載進來的吧~
可以看到new DexClassLoader的時候我們用了4個參數,參數意義上面已經講過了,從上面的源碼中可以看到DexClassLoader的構造器中直接調用了父類的構造器,只是將optimizedDirectory路徑封裝成一個File,具體這些參數是如何被使用的呢,我們往下看。
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 libraryPath 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 libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
首先也是調用了父類的構造器,但這里只將parent傳給父類,即ClassLoader,ClassLoader中做的也很很簡單,它內部有個parent屬性,正好保存傳進來的參數parent,這里可以稍微看一下第二個參數的注釋,最后一句說到可以為null,而是否為null又剛好是PathClassLoader和DexClassLoader的區別,那是否為null最終又意味著什么呢?賣個關子先~
protected ClassLoader(ClassLoader parentLoader) {
this(parentLoader, false);
}
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}
接下去BaseDexClassLoader給originalPath 和 pathList賦了值,originalPath就是我們傳進入的dex文件路徑,pathList 是一個new 出來的DexPathList對象,這個DexPathList又是何方神圣?且聽慢慢道來。
/**
* Constructs an instance.
*
* @param definingContext the context in which any as-yet unresolved
* classes should be defined
* @param dexPath list of dex/resource path elements, separated by
* {@code File.pathSeparator}
* @param libraryPath list of native library directory path elements,
* separated by {@code File.pathSeparator}
* @param optimizedDirectory directory where optimized {@code .dex} files
* should be found and written to, or {@code null} to use the default
* system directory for same
*/
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
this.dexElements =
makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
別的先不說,先看注釋。第四個參數中說到如果optimizedDirectory 為null則使用系統默認路徑代替,這個默認路徑也就是/data/dalvik-cache/目錄,這個一般情況下是沒有權限去訪問的,所以這也就解釋了為什么我們只能用DexClassLoader去加載類而不用PathClassLoader。
然后接著看代碼,顯然,前面三個if判斷都是用來驗證參數的合法性的,之后同樣只是做了三個賦值操作,第一個就不說了,保存了實例化DexPathList的classloader,第二個參數的聲明是一個Element數組,第三個參數是lib庫的目錄文件數組,
private final Element[] dexElements;
private final File[] nativeLibraryDirectories;
看它們之前先看看幾個split小函數:
private static ArrayList<File> splitDexPath(String path) {
return splitPaths(path, null, false);
}
private static File[] splitLibraryPath(String path) {
ArrayList<File> result = splitPaths(
path, System.getProperty("java.library.path", "."), true);
return result.toArray(new File[result.size()]);
}
這兩個顧名思義就是拿來分割dexPath和libPath,它們內部都調用了splitPaths方法,只是三個參數不一樣,其中splitLibraryPath方法中調用splitPaths時的第二個參數仿佛又透露了什么信息,沒錯,之前介紹DexClassLoader參數中的libraryPath的時候說過,會加上系統so庫的存放目錄,就是在這個時候添加上去的。
private static ArrayList<File> splitPaths(String path1, String path2,
boolean wantDirectories) {
ArrayList<File> result = new ArrayList<File>();
splitAndAdd(path1, wantDirectories, result);
splitAndAdd(path2, wantDirectories, result);
return result;
}
什么啊,原來這個方法也沒做什么事啊,只是把參數path1和參數path2又分別調用了下splitAndAdd方法,但是這里創建了一個ArrayList,而且調用splitAndAdd方法的時候都當參數傳入了,并且最終返回了這個list,所以我們大膽猜測下,path1和path2最后被分割后的值都存放在了list中返回,看下是不是這么一回事吧:
private static void splitAndAdd(String path, boolean wantDirectories,
ArrayList<File> resultList) {
if (path == null) {
return;
}
String[] strings = path.split(Pattern.quote(File.pathSeparator));
for (String s : strings) {
File file = new File(s);
if (! (file.exists() && file.canRead())) {
continue;
}
/*
* Note: There are other entities in filesystems than
* regular files and directories.
*/
if (wantDirectories) {
if (!file.isDirectory()) {
continue;
}
} else {
if (!file.isFile()) {
continue;
}
}
resultList.add(file);
}
}
果然,跟我們猜的一樣,只是又加上了文件是否存在以及是否可讀的驗證,然后根據參數wantDirectories判斷是否文件類型是被需要的類型,最終加入list。現在我們回過頭去看看splitDexPath方法和splitLibraryPath方法,是不是一目了然了。
再往上看DexPathList的構造器,nativeLibraryDirectories的最終值也已經知道了,就差dexElements了,makeDexElements方法的兩個參數我們也已經知道了,那我們就看看makeDexElements都干了些什么吧。
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
/*
* Note: ZipException (a subclass of IOException)
* might get thrown by the ZipFile constructor
* (e.g. if the file isn't actually a zip/jar
* file).
*/
System.logE("Unable to open zip file: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
/*
* IOException might get thrown "legitimately" by
* the DexFile constructor if the zip file turns
* out to be resource-only (that is, no
* classes.dex file in it). Safe to just ignore
* the exception here, and let dex == null.
*/
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
方法也不長,我們一段段看下去。首先創建了一個elememt 列表,然后遍歷由dexpath分割得來的文件列表,其實一般使用場景下也就一個文件。循環里面針對每個file 聲明一個zipfile和一個dexfile,判斷file的文件后綴名,如果是".dex"則使用loadDexFile方法給dex賦值,如果是“.apk”,“.jar”,“.zip”文件的則把file包裝成zipfile賦值給zip,然后同樣是用loadDexFile方法給dex賦值,如果是其他情況則不做處理,打印日志說明文件類型不支持,而且下一個if判斷中由于zip和dex都未曾賦值,所以也不會添加到elements列表中去。注意下:這里所謂的文件類型僅僅是指文件的后綴名而已,并不是文件的實際類型,比如我們將.zip文件后綴改成.txt,那么就不支持這個文件了。而且我們可以看到對于dexpath目前只支持“.dex”、“.jar”、“.apk”、“.zip”這四種類型。
現在還剩下兩個東西可能還不太明確,一個是什么是DexFile以及這里的loadDexFile方法是如何創建dexfile實例的,另一個是什么是Elememt,看了下Element源碼,哈哈,so easy,就是一個簡單的數據結構體加一個方法,所以我們就先簡單把它當做一個存儲了file,zip,dex三個字段的一個實體類。那么就剩下DexFile了。
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
很簡潔,如果optimizedDirectory == null則直接new 一個DexFile,否則就使用DexFile.loadDex來創建一個DexFile實例。
/**
* Converts a dex/jar file path and an output directory to an
* output file path for an associated optimized dex file.
*/
private static String optimizedPathFor(File path,
File optimizedDirectory) {
/*
* Get the filename component of the path, and replace the
* suffix with ".dex" if that's not already the suffix.
*
* We don't want to use ".odex", because the build system uses
* that for files that are paired with resource-only jar
* files. If the VM can assume that there's no classes.dex in
* the matching jar, it doesn't need to open the jar to check
* for updated dependencies, providing a slight performance
* boost at startup. The use of ".dex" here matches the use on
* files in /data/dalvik-cache.
*/
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
這個方法獲取被加載的dexpath的文件名,如果不是“.dex”結尾的就改成“.dex”結尾,然后用optimizedDirectory和新的文件名構造一個File并返回該File的路徑,所以DexFile.loadDex方法的第二個參數其實是dexpath文件對應的優化文件的輸出路徑。比如我要加載一個dexpath為“sdcard/coder_yu/plugin.apk”,optimizedDirectory 為使用范例中的目錄的話,那么最終優化后的輸出路徑為/data/user/0/com.coder_yu.test/app_dex/plugin.dex,具體的目錄在不同機型不同rom下有可能會不一樣。
是時候看看DexFile了。在上面的loadDexFile方法中我們看到optimizedDirectory參數為null的時候直接返回new DexFile(file)了,否則返回 DexFile.loadDex(file.getPath(), optimizedPath, 0),但其實他們最終都是使用了相同方法去加載dexpath文件,因為DexFile.loadDex方法內部也是直接調用的了DexFile的構造器,以下:
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags);
}
然后看看DexFile的構造器吧
public DexFile(File file) throws IOException {
this(file.getPath());
}
/**
* Opens a DEX file from a given filename. This will usually be a ZIP/JAR
* file with a "classes.dex" inside.
*
* The VM will generate the name of the corresponding file in
* /data/dalvik-cache and open it, possibly creating or updating
* it first if system permissions allow. Don't pass in the name of
* a file in /data/dalvik-cache, as the named file is expected to be
* in its original (pre-dexopt) state.
*
* @param fileName
* the filename of the DEX file
*
* @throws IOException
* if an I/O error occurs, such as the file not being found or
* access rights missing for opening it
*/
public DexFile(String fileName) throws IOException {
mCookie = openDexFile(fileName, null, 0);
mFileName = fileName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie);
}
private DexFile(String sourceName, String outputName, int flags) throws IOException {
mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie);
}
可以看到直接new DexFile(file)和DexFile.loadDex(file.getPath(), optimizedPath, 0)最終都是調用了openDexFile(sourceName, outputName, flags)方法,只是直接new的方式optimizedPath參數為null,這樣openDexFile方法會默認使用 /data/dalvik-cache目錄作為優化后的輸出目錄,第二個構造器的注釋中寫的很明白了。mCookie是一個int值,保存了openDexFile方法的返回值,openDexFile方法是一個native方法,我們就不深入了,我自己也就看了個大概,有興趣的同學可以看下這篇文章:
http://blog.csdn.net/zhoushishang/article/details/38703623
這樣總算是創建了一個DexClassLoader。
我們回顧一下:從new DexClassLoader(dexPath,optimizedDirectory,libraryPath,parentLoader)開始,調用父類BaseDexClassLoader構造器,用originalPath 保存了 dexPath,pathList保存了一個由dexPath、optimizedDirectory、libraryPath、loader四個參數構建的DexPathList,DexPathList中的definingContext 保存了parentLoader,optimizedDirectory和libraryPath會被分割成數組,其中nativeLibraryDirectories保存了libraryPath被分割后的數組,并且加上了系統so庫的目錄,dexElements保存了由dexPath被分割后的對應的file而創建的Elememt,它只是一個簡單實體類,由一個File,一個ZipFile,一個DexFile組成,ZipFile是由jar、zip、apk形式的file包裝成而來,DexFile使用native方法openDexFile打開了具體的file并輸出到優化路徑。
DexClassLoader的創建過程我們已經看完了,看看它是如何去找到我們需要的類的吧!
先把使用范例再貼一遍
try {
File file = view.getActivity().getDir("dex",0);
String optimizedDirectory = file.getAbsolutePath();
DexClassLoader loader = new DexClassLoader("需要被加載的dex文件所在的路徑",optimizedDirectory,null,context.getClassLoader());
loader.loadClass("需要加載的類的完全限定名");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
我們已經拿到loader了,然后就是loader.loadClass("需要加載的類的完全限定名"),我們假設需要被加載的類為 'com.coder_yu.plugin.Test',也即是loader.loadClass("com.coder_yu.plugin.Test"),所以我們先去DexClassLoader中看看loaderClass方法,不過好像沒有這個方法,那就去它的父類BaseDexClassLoader中看看,還是 沒有,那就再往上走,去ClassLoader類里面找,總算找到了:
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
直接調用另一個loadclass方法,看一下:
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
直接調用findLoadedClass方法,這個方法會去虛擬機中找當前classloader是否已經加載過該class了,如果加載過了則直接返回這個class,否則返回null,并去調用parent的loadClass方法,依次類推,會一直找到根classloader的findLoadedClass方法,因為我們這里是第一次去加載sd卡上的自定義插件,所以肯定沒被加載過,那么直到根classloader.findLoadedClass將一直返回null,然后會去調 findClass(className)方法,我們先看一下findLoadedClass方法吧:
protected final Class<?> findLoadedClass(String className) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
return VMClassLoader.findLoadedClass(loader, className);
}
BootClassLoader是ClassLoader的默認parentloader,所以它是加載器鏈中的頂端,也就是classloader的根節點,因此它會最終代理一些方法到虛擬機去執行,感興趣的同學可以去看下ClassLoader的源碼。這里說一下,其實根節點我們剛才說了是BootClassLoader,而且它重寫了loadClass方法,因為它沒有parentloader,所以它沒有再去調用parent.loadClass(className, false),而是直接調用findClass方法,而findClass方法中也是直接返回Class.classForName(name, false, null)得到的值,顯然結果是null。
我們構造DexClassLoader的時候給的parentloader參數是context.getClassLoader(),它其實就是加載我們應用的類加載器,也就是PathClassLoader,它的parentloader就是BootClassLoader,這個具體代碼我沒去找,可以簡單的打印下日志就能發現,不過這個情況在instant run 的情況下會有所不同,大家可以去研究研究~有點扯遠了,這里我想說的是我們傳入的應用類加載器中通過findClass也是加載不到我們需要的類的,所以我們還是得去看我們自定義的DexClassLoader中的findClass方法,DexClassLoader中還是沒有重寫這個方法,那么還是去BaseDexClassLoader中看吧:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
還記得我們的pathList嗎?不記得回頭看看上面的圖:
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
Element是不是也很熟悉?就是那個簡單的實體類,所以繼續到DexFile中去看dex.loadClassBinaryName方法:
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
最終使用了native方法defineClass去加載我們需要的類,注意參數里有個mCookie,它就是openDexFile方法返回的dex文件返回的int值,這樣就將我們需要加載的類和我們的dex文件對應起來了,去從我們給定的dex文件中去加載我們需要的類。
至此,我們的加載過程就很清楚了。本文所提及的內容并沒有什么技術含量,只是順著一條主線梳理了下整個過程,對于搞明白一些細節可能還是有點幫助。順帶說一下,其實熱修復也是用到了這個加載機制,動態的替換掉有bug的類,因為已經被加載過的類不會再被加載,所以只要我們把修復bug后的類放到有bug那個類之前加載,就能解決問題了。