一. 概述
Android從5.0開始就采用art虛擬機, 該虛擬機有些類似JAVA虛擬機, 程序運行過程也需要通過ClassLoader 將目標(biāo)類加載到內(nèi)存.
傳統(tǒng)Jvm主要是通過讀取class字節(jié)碼來加載, 而art則是從dex字節(jié)碼來讀取. 這是一種更為優(yōu)化的方案, 可以將多個.class文件合并成一個classes.dex文件. 下面直接來看看ClassLoader的關(guān)系。
二. 五種類構(gòu)造器
接下來依次看看PathClassLoader,DexClassLoader,BaseDexClassLoader,BootClassLoader,ClassLoader這5個類加載器。
PathClassLoader和DexClassLoader,它們都繼承自BaseDexClassLoader,這兩個類有什么區(qū)別呢?其實看一下它們的源碼注釋就一目了然了。因為代碼很少,約等于沒有,這里直接貼出它們的源碼,其實主要是注釋:
2.1 PathClassLoader
由注釋看可以發(fā)現(xiàn)PathClassLoader被用來加載本地文件系統(tǒng)上的文件或目錄,但不能從網(wǎng)絡(luò)上加載,關(guān)鍵是它被用來加載系統(tǒng)類和我們的應(yīng)用程序,這也是為什么它的兩個構(gòu)造函數(shù)中調(diào)用父類構(gòu)造器的時候第二個參數(shù)傳null,具體的參數(shù)意義請看接下來DexClassLoader的注釋。
2.2 DexClassLoader
DexClassLoader用來加載jar、apk,其實還包括zip文件或者直接加載dex文件,它可以被用來執(zhí)行未安裝的代碼或者未被應(yīng)用加載過的代碼。這里也寫出了它需要的四個參數(shù)的意思
- dexPath:需要被加載的文件地址,可以多個,用File.pathSeparator分割
- optimizedDirectory:dex文件被加載后會被編譯器優(yōu)化,優(yōu)化之后的dex存放路徑,不可以為null。注意,注釋中也提到需要一個應(yīng)用私有的可寫的一個路徑,以防止應(yīng)用被注入攻擊,并且給出了例子 File dexOutputDir = context.getDir("dex", 0);
- libraryPath:包含libraries的目錄列表,plugin中有so文件,需要將so拷貝到sd卡上,然后把so所在的目錄當(dāng)參數(shù)傳入,同樣用File.pathSeparator分割,如果沒有則傳null就行了,會自動加上系統(tǒng)so庫的存放目錄
- parent:父類構(gòu)造器
這里著重看一下第二個參數(shù),之前說過PathClassLoader中調(diào)用父類構(gòu)造器的時候這個參數(shù)穿了null,因為加載App應(yīng)用的時候我們的apk已經(jīng)被安裝到本地文件系統(tǒng)上了,其內(nèi)部的dex已經(jīng)被提取并且執(zhí)行過優(yōu)化了,優(yōu)化之后放在系統(tǒng)目錄/data/dalvik-cache下。
2.3 BaseDexClassLoader
BaseDexClassLoader構(gòu)造函數(shù), 有一個非常重要的過程, 那就是初始化DexPathList對象.
另外該構(gòu)造函數(shù)的參數(shù)說明:
- dexPath: 包含目標(biāo)類或資源的apk/jar列表;當(dāng)有多個路徑則采用:分割;
- optimizedDirectory: 優(yōu)化后dex文件存在的目錄, 可以為null;
- libraryPath: native庫所在路徑列表;當(dāng)有多個路徑則采用:分割;
- ClassLoader:父類的類加載器.
2.4 BootClassLoader
2.5 ClassLoader
再來看看SystemClassLoader,這里的getSystemClassLoader()返回的是PathClassLoader類。
3. 類加載實例
首先看一段如何使用類加載器加載的調(diào)用代碼:
1 try { 2 File file = view.getActivity().getDir("dex",0); 3 String optimizedDirectory = file.getAbsolutePath(); 4 DexClassLoader loader = new DexClassLoader("需要被加載的dex文件所在的路徑",optimizedDirectory,null,context.getClassLoader()); 5 loader.loadClass("需要加載的類的完全限定名"); 6 } catch (ClassNotFoundException e) { 7 e.printStackTrace(); 8 }
這里我們就用了自定義了一個DexClassLoaderLoader,并且調(diào)用了它的loadClass方法,這樣一個需要被使用的類就被我們加載進來了,接下去就可以正常使用這個類了,具體怎么使用我就不多說了,我們還是來研究研究這個類是怎么被加載進來的吧~
可以看到new DexClassLoader的時候我們用了4個參數(shù),參數(shù)意義上面已經(jīng)講過了,從上面的源碼中可以看到DexClassLoader的構(gòu)造器中直接調(diào)用了父類的構(gòu)造器,只是將optimizedDirectory路徑封裝成一個File,具體這些參數(shù)是如何被使用的呢,我們往下看。
BaseDexClassLoader類的構(gòu)造器
首先也是調(diào)用了父類的構(gòu)造器,但這里只將parent傳給父類,即ClassLoader,ClassLoader中做的也很很簡單,它內(nèi)部有個parent屬性,正好保存?zhèn)鬟M來的參數(shù)parent,這里可以稍微看一下第二個參數(shù)的注釋,最后一句說到可以為null,而是否為null又剛好是PathClassLoader和DexClassLoader的區(qū)別,那是否為null最終又意味著什么呢?
接下來BaseDexClassLoader給originalPath 和 pathList賦了值,originalPath就是我們傳進入的dex文件路徑,pathList 是一個new 出來的DexPathList對象。
別的先不說,先看注釋。第四個參數(shù)中說到如果optimizedDirectory 為null則使用系統(tǒng)默認(rèn)路徑代替,這個默認(rèn)路徑也就是/data/dalvik-cache/目錄,這個一般情況下是沒有權(quán)限去訪問的,所以這也就解釋了為什么我們只能用DexClassLoader去加載類而不用PathClassLoader。
然后接著看代碼,顯然,前面三個if判斷都是用來驗證參數(shù)的合法性的,之后同樣只是做了三個賦值操作,第一個就不說了,保存了實例化DexPathList的classloader,第二個參數(shù)的聲明是一個Element數(shù)組,第三個參數(shù)是lib庫的目錄文件數(shù)組。
看它們之前先看看幾個split小函數(shù):
這兩個顧名思義就是拿來分割dexPath和libPath,它們內(nèi)部都調(diào)用了splitPaths方法,只是三個參數(shù)不一樣,其中splitLibraryPath方法中調(diào)用splitPaths時的第二個參數(shù)仿佛又透露了什么信息,沒錯,之前介紹DexClassLoader參數(shù)中的libraryPath的時候說過,會加上系統(tǒng)so庫的存放目錄,就是在這個時候添加上去的。
什么啊,原來這個方法也沒做什么事啊,只是把參數(shù)path1和參數(shù)path2又分別調(diào)用了下splitAndAdd方法,但是這里創(chuàng)建了一個ArrayList,而且調(diào)用splitAndAdd方法的時候都當(dāng)參數(shù)傳入了,并且最終返回了這個list,所以我們大膽猜測下,path1和path2最后被分割后的值都存放在了list中返回,看下是不是這么一回事吧:
果然,跟我們猜的一樣,只是又加上了文件是否存在以及是否可讀的驗證,然后根據(jù)參數(shù)wantDirectories判斷是否文件類型是被需要的類型,最終加入list。現(xiàn)在我們回過頭去看看splitDexPath方法和splitLibraryPath方法,是不是一目了然了。
再往上看DexPathList的構(gòu)造器,nativeLibraryDirectories的最終值也已經(jīng)知道了,就差dexElements了,makeDexElements方法的兩個參數(shù)我們也已經(jīng)知道了,那我們就看看makeDexElements都干了些什么吧。
方法也不長,我們一段段看下去。首先創(chuàng)建了一個elememt 列表,然后遍歷由dexpath分割得來的文件列表,其實一般使用場景下也就一個文件。循環(huán)里面針對每個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”這四種類型。
現(xiàn)在還剩下兩個東西可能還不太明確,一個是什么是DexFile以及這里的loadDexFile方法是如何創(chuàng)建dexfile實例的,另一個是什么是Elememt,看了下Element源碼,哈哈,so easy,就是一個簡單的數(shù)據(jù)結(jié)構(gòu)體加一個方法,所以我們就先簡單把它當(dāng)做一個存儲了file,zip,dex三個字段的一個實體類。那么就剩下DexFile了。
很簡潔,如果optimizedDirectory == null則直接new 一個DexFile,否則就使用DexFile.loadDex來創(chuàng)建一個DexFile實例。
這個方法獲取被加載的dexpath的文件名,如果不是“.dex”結(jié)尾的就改成“.dex”結(jié)尾,然后用optimizedDirectory和新的文件名構(gòu)造一個File并返回該File的路徑,所以DexFile.loadDex方法的第二個參數(shù)其實是dexpath文件對應(yīng)的優(yōu)化文件的輸出路徑。比如我要加載一個dexpath為“sdcard/coder_yu/plugin.apk”,optimizedDirectory 為使用范例中的目錄的話,那么最終優(yōu)化后的輸出路徑為/data/user/0/com.coder_yu.test/app_dex/plugin.dex,具體的目錄在不同機型不同rom下有可能會不一樣。
是時候看看DexFile了。在上面的loadDexFile方法中我們看到optimizedDirectory參數(shù)為null的時候直接返回new DexFile(file)了,否則返回 DexFile.loadDex(file.getPath(), optimizedPath, 0),但其實他們最終都是使用了相同方法去加載dexpath文件,因為DexFile.loadDex方法內(nèi)部也是直接調(diào)用的了DexFile的構(gòu)造器,以下:
然后看看DexFile的構(gòu)造器吧
可以看到直接new DexFile(file)和DexFile.loadDex(file.getPath(), optimizedPath, 0)最終都是調(diào)用了openDexFile(sourceName, outputName, flags)方法,只是直接new的方式optimizedPath參數(shù)為null,這樣openDexFile方法會默認(rèn)使用 /data/dalvik-cache目錄作為優(yōu)化后的輸出目錄,第二個構(gòu)造器的注釋中寫的很明白了。mCookie是一個int值,保存了openDexFile方法的返回值,openDexFile方法是一個native方法,我們就不深入了。
四. 總結(jié)
幾種類加載器:
- PathClassLoader: 主要用于系統(tǒng)和app的類加載器,其中optimizedDirectory為null, 采用默認(rèn)目錄/data/dalvik-cache/
- DexClassLoader: 可以從包含classes.dex的jar或者apk中,加載類的類加載器, 可用于執(zhí)行動態(tài)加載,但必須是app私有可寫目錄來緩存odex文件. 能夠加載系統(tǒng)沒有安裝的apk或者jar文件, 因此很多插件化方案都是采用DexClassLoader;
- BaseDexClassLoader: 比較基礎(chǔ)的類加載器, PathClassLoader和DexClassLoader都只是在構(gòu)造函數(shù)上對其簡單封裝而已.
- BootClassLoader: 作為父類的類構(gòu)造器。
熱修復(fù)核心邏輯:在DexPathList.findClass()過程,一個Classloader可以包含多個dex文件,每個dex文件被封裝到一個Element對象,這些Element對象排列成有序的數(shù)組dexElements。當(dāng)查找某個類時,會遍歷所有的dex文件,如果找到則直接返回,不再繼續(xù)遍歷dexElements。也就是說當(dāng)兩個類不同的dex中出現(xiàn),會優(yōu)先處理排在前面的dex文件,這便是熱修復(fù)的核心精髓,將需要修復(fù)的類所打包的dex文件插入到dexElements前面。
類加載過程常見的ClassNotFound原因:
- ABI異常:常見在系統(tǒng)APP,為了減小system分區(qū)大小會將apk源文件中的classes.dex文件移除,對于既然可運行在64位又可運行在32位模式的應(yīng)用,當(dāng)被強制設(shè)置32位時,openDexFileNative在查找不到oat文件時會運行在解釋模式,而classes.dex文件不再則出現(xiàn)ClassNotFound異常。
- MultiDex處理不當(dāng),由于每個Dex文件中方法個數(shù)不能超過65536,引入MultiDex機制。dex2oat會自動查找Apk文件中的classes.dex,classes2.dex,…classesN.dex等文件,編譯到/data/dalvik-cache下生成oat文件。這里需要文件名跟classesN.dex格式,并且一定要與classes.dex一起放置在第一級目錄,有些APP不按照要求來,導(dǎo)致ClassNotFound異常。
好啦,文章寫到這里就結(jié)束了,如果你覺得文章寫得不錯就給個贊唄?如果你覺得那里值得改進的,請給我留言。一定會認(rèn)真查詢,修正不足。謝謝。