ClassLoader類加載與熱修復(fù)
ART 和 Dalvik
DVM也是實(shí)現(xiàn)了JVM規(guī)范的一個(gè)虛擬器,默認(rèn)使用CMS垃圾回收器,但是與JVM運(yùn)行 Class 字節(jié)碼不同攻柠,DVM 執(zhí)行 Dex(Dalvik Executable Format) ——專為 Dalvik 設(shè)計(jì)的一種壓縮格式赃磨。Dex 文件是很多 .class 文件處理壓 縮后的產(chǎn)物立由,最終可以在 Android 運(yùn)行時(shí)環(huán)境執(zhí)行。
而ART(Android Runtime) 是在 Android 4.4 中引入的一個(gè)開發(fā)者選項(xiàng)序厉,也是 Android 5.0 及更高版本的默認(rèn) Android 運(yùn)行時(shí)锐膜。ART 和 Dalvik 都是運(yùn)行 Dex 字節(jié)碼的兼容運(yùn)行時(shí),因此針對 Dalvik 開發(fā)的應(yīng)用也能在 ART 環(huán) 境中運(yùn)作弛房。
dexopt與dexaot
- dexopt
在Dalvik中虛擬機(jī)在加載一個(gè)dex文件時(shí)道盏,對 dex 文件 進(jìn)行 驗(yàn)證 和 優(yōu)化的操作,其對 dex 文件的優(yōu)化結(jié)果 變成了 odex(Optimized dex) 文件文捶,這個(gè)文件和 dex 文件很像荷逞,只是使用了一些優(yōu)化操作碼。
- dex2oat
ART 預(yù)先編譯機(jī)制粹排,在安裝時(shí)對 dex 文件執(zhí)行AOT 提前編譯操作种远,編譯為OAT(實(shí)際上是ELF文件)可執(zhí)行 文件(機(jī)器碼)。
ClassLoader介紹
任何一個(gè) Java 程序都是由一個(gè)或多個(gè) class 文件組成顽耳,在程序運(yùn)行時(shí)坠敷,需要將class文件加載到 JVM 中才可以使用,負(fù)責(zé)加載這些 class 文件的就是 Java 的類加載機(jī)制射富。ClassLoader 的作用簡單來說就是加載 class 文件膝迎,提供 給程序運(yùn)行時(shí)使用。每個(gè) Class 對象的內(nèi)部都有一個(gè) classLoader 字段來標(biāo)識自己是由哪個(gè) ClassLoader 加載的胰耗。
class Class<T> {
...
private transient ClassLoader classLoader;
...
}
ClassLoader是一個(gè)抽象類限次,而它的具體實(shí)現(xiàn)類主要有:
-
BootClassLoader
用于加載Android Framework層class文件。
-
PathClassLoader
用于Android應(yīng)用程序類加載器柴灯〉嗨。可以加載指定的dex,以及jar弛槐、zip懊亡、apk中的classes.dex
-
DexClassLoader
用于加載指定的dex,以及jar乎串、zip店枣、apk中的classes.dex
很多博客里說PathClassLoader只能加載已安裝的apk的dex,其實(shí)這說的應(yīng)該是在dalvik虛擬機(jī)上
但現(xiàn)在一般不用關(guān)心dalvik了叹誉。
Log.e(TAG, "Activity.class 由:" + Activity.class.getClassLoader() +" 加載");
Log.e(TAG, "MainActivity.class 由:" + getClassLoader() +" 加載");
//輸出:
Activity.class 由:java.lang.BootClassLoader@d3052a9 加載
MainActivity.class 由:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.enjoyfix-1/base.apk"],nativeLibraryDirectories= [/data/app/com.enjoy.enjoyfix-1/lib/x86, /system/lib, /vendor/lib]]] 加載
PathClassLoader 與 DexClassLoader 的共同父類是 BaseDexClassLoader 鸯两。
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);
}
}
可以看到兩者唯一的區(qū)別在于:創(chuàng)建 DexClassLoader 需要傳遞一個(gè) optimizedDirectory 參數(shù),并且會將其創(chuàng)建 為 File 對象傳給 super 长豁,而 PathClassLoader 則直接給到null钧唐。因此兩者都可以加載指定的dex,以及jar匠襟、 zip钝侠、apk中的classes.dex
PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());
File dexOutputDir = context.getCodeCacheDir();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());
其實(shí), optimizedDirectory 參數(shù)就是dexopt的產(chǎn)出目錄(odex)该园。那 PathClassLoader 創(chuàng)建時(shí),這個(gè)目錄為null帅韧,就 意味著不進(jìn)行dexopt?并不是里初, optimizedDirectory 為null時(shí)的默認(rèn)路徑為:/data/dalvik-cache。
在API 26源碼中忽舟,將DexClassLoader的optimizedDirectory標(biāo)記為了 deprecated 棄用双妨,實(shí)現(xiàn)也變?yōu)榱?
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}和PathClassLoader一摸一樣了!
雙親委托機(jī)制
可以看到創(chuàng)建 ClassLoader 需要接收一個(gè) ClassLoader parent 參數(shù)。這個(gè) parent 的目的就在于實(shí)現(xiàn)類加載的雙 親委托叮阅。即:
某個(gè)類加載器在接到加載類的請求時(shí)刁品,首先將加載任務(wù)委托給父類加載器,依次遞歸浩姥,如果父類加載器可以完成類 加載任務(wù)哑诊,就成功返回;只有父類加載器無法完成此加載任務(wù)時(shí),才自己去加載及刻。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
// 檢查class是否有被加載
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { //如果parent不為null,則調(diào)用parent的loadClass進(jìn)行加載
c = parent.loadClass(name, false);
} else { //parent為null竞阐,則調(diào)用BootClassLoader進(jìn)行加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果都找不到就自己查找
long t1 = System.nanoTime(); c = findClass(name);
}
}
return c;
}
因此我們自己創(chuàng)建的ClassLoader: new PathClassLoader("/sdcard/xx.dex", getClassLoader()); 并不僅僅 只能加載 xx.dex中的class缴饭。
值得注意的是: c = findBootstrapClassOrNull(name);
按照方法名理解,應(yīng)該是當(dāng)parent為null時(shí)候骆莹,也能夠加載 BootClassLoader 加載的類颗搂。
new PathClassLoader("/sdcard/xx.dex", null) ,能否加載Activity.class?
但是實(shí)際上幕垦,Android當(dāng)中的實(shí)現(xiàn)為:(Java不同)
findClass
可以看到在所有父ClassLoader無法加載Class時(shí)丢氢,則會調(diào)用自己的 findClass 方法。 findClass 在ClassLoader中的定義為:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
其實(shí)任何ClassLoader子類先改,都可以重寫 loadClass 與 findClass 疚察。一般如果你不想使用雙親委托,則重寫 loadClass 修改其實(shí)現(xiàn)仇奶。而重寫 findClass 則表示在雙親委托下貌嫡,父ClassLoader都找不到Class的情況下,定義 自己如何去查找一個(gè)Class该溯。而我們的 PathClassLoader 會自己負(fù)責(zé)加載 MainActivity 這樣的程序中自己編寫的類岛抄,利用雙親委托父ClassLoader加載Framework中的 Activity 。說明 PathClassLoader 并沒有重寫 loadClass 狈茉,因此我們可以來看看PathClassLoader中的 findClass 是如何實(shí)現(xiàn)的夫椭。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,String
librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath,optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//查找指定的class
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);
}
}
return c;
}
實(shí)現(xiàn)非常簡單,從 pathList 中查找class氯庆。繼續(xù)查看 DexPathLis
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
//.........
// splitDexPath 實(shí)現(xiàn)為返回 List<File>.add(dexPath)
// makeDexElements 會去 List<File>.add(dexPath) 中使用DexFile加載dex文件返回 Element數(shù)組
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
}
public Class findClass(String name, List<Throwable> suppressed) {
//從element中獲得代表Dex的 DexFile
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
//查找class
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
熱修復(fù)
PathClassLoader 中存在一個(gè)Element數(shù)組蹭秋,Element類中存在一個(gè)dexFile成員表示dex文件扰付,即:APK中有X個(gè) dex,則Element數(shù)組就有X個(gè)元素感凤。
在 PathClassLoader 中的Element數(shù)組為:[patch.dex , classes.dex , classes2.dex]悯周。如果存在Key.class位于 patch.dex與classes2.dex中都存在一份,當(dāng)進(jìn)行類查找時(shí)陪竿,循環(huán)獲得 dexElements 中的DexFile禽翼,查找到了 Key.class則立即返回,不會再管后續(xù)的element中的DexFile是否能加載到Key.class了族跛。
因此實(shí)際上闰挡,一種熱修復(fù)實(shí)現(xiàn)可以將出現(xiàn)Bug的class單獨(dú)的制作一份fix.dex文件(補(bǔ)丁包),然后在程序啟動時(shí)礁哄,從 服務(wù)器下載fix.dex保存到某個(gè)路徑长酗,再通過fix.dex的文件路徑,用其創(chuàng)建 Element 對象桐绒,然后將這個(gè) Element 對 象插入到我們程序的類加載器 PathClassLoader 的 pathList 中的 dexElements 數(shù)組頭部夺脾。這樣在加載出現(xiàn)Bug的 class時(shí)會優(yōu)先加載fix.dex中的修復(fù)類,從而解決Bug茉继。