一.概述
通過本篇文章的學習,你將學會:
1.java中類加載機制
2.Android中類加載機制
二.java類加載機制
java類加載流程
JVM類加載機制分為五個部分:加載,驗證,準備,解析,初始化。如下圖所示:
這是一個類從加載到使用及卸載的全部生命周期。
加載
根據一個類的全限定名(如cn.edu.hdu.test.HelloWorld.class)來讀取此類的二進制字節流到JVM內部,轉換為一個與目標類型對應的java.lang.Class對象,也可以從ZIP包中讀取(比如從jar包和war包中讀?。?。
驗證
為了確保Class文件的字節流中包含的信息是否符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。包括文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。
準備
為類中的所有靜態變量分配內存空間,并為其設置一個初始值(類變量即static修飾的變量,且是數據類型的零值,除非被final修飾,比如static int a=1,初始值為0 ,如果是static final int=1,初始值就是1,由于還沒有產生對象,實例變量將不再此操作范圍內)。
解析
解析階段是指虛擬機將常量池中的符號引用替換為直接引用的過程。
初始化
初始化階段是類加載最后一個階段,前面的類加載階段之后,除了在加載階段可以自定義類加載器以外,其它操作都由JVM主導。到了初始階段,才開始真正執行類中定義的Java程序代碼。初始化階段是執行類構造器<client>方法的過程。
父類委托
先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類
緩存機制
緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統才會讀取該類對應的二進制數據,并將其轉換成Class對象,存入緩存區。
java類被jvm加載的過程
靜態代碼塊執行前,靜態變量就已經執行了;非靜態代碼塊執行前,非靜態變量就已經執行了。
父類的靜態成員變量—->父類靜態代碼塊—->子類靜態成員變量—->子類靜態代碼塊—>父類非靜態變量—->父類非靜態代碼塊—->父類構造方法—->子類非靜態變量—->子類非靜態代碼塊—->子類構造方法。
注意
在類第一次調用時,靜態代碼塊只執行這一次。
靜態代碼塊和靜態方法只能調用靜態變量,因為它們執行時非靜態變量還沒初始化;
非靜態代碼塊和非靜態方法可以調用任何(靜態+非靜態)變量。
類加載器
- 啟動類加載器,Bootstrap ClassLoader,加載JACA_HOME\lib,或者被-Xbootclasspath參數限定的類
- 擴展類加載器,Extension ClassLoader,加載\lib\ext,或者被java.ext.dirs系統變量指定的類
- 應用程序類加載器,Application ClassLoader,加載ClassPath中的類庫
-
自定義類加載器,通過繼承ClassLoader實現,一般是加載我們的自定義類
image.png
雙親委派模型
雙親委派是指每次收到類加載請求時,先將請求委派給父類加載器完成(所有加載請求最終會委派到頂層的Bootstrap ClassLoader加載器中),如果父類加載器無法完成這個加載(該加載器的搜索范圍中沒有找到對應的類),子類嘗試自己加載。這樣的好處可以避免一個類被多次加載。
我們看一下loadClass方法:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先從緩存查找該class對象,找到就不用重新加載
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果找不到,則委托給父類加載器去加載
c = parent.loadClass(name, false);
} else {
//如果沒有父類,則委托給啟動加載器去加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果都沒有找到,則通過自定義實現的findClass去查找并加載
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {//是否需要在加載時進行解析
resolveClass(c);
}
return c;
}
}
緩存 -> 父類加載器 -> 沒有父類 -> 啟動類加載器 -> 自己的 findClass() 方法查找和加載
三.Android類加載機制
在Java標準的虛擬機中,類加載可以從class文件中讀取,也可以是其他形式的二進制流。而Dalvik虛擬機如同Java虛擬機一樣,在運行程序時首先需要將對應的類加載到內存中。只不過Android平臺上虛擬機運行的是Dex字節碼,一種對class文件優化的產物,傳統Class文件是一個Java源碼文件會生成一個.class文件,而Android是把所有Class文件進行合并,優化,然后生成一個最終的class.dex,目的是把不同class文件重復的東西只需保留一份,如果我們的Android應用不進行分dex處理,最后一個應用的apk只會有一個dex文件。
Android平臺的類加載器
Android中類加載器有BootClassLoader,URLClassLoader,PathClassLoader,DexClassLoader,BaseDexClassLoader,等都最終繼承自java.lang.ClassLoader。如圖:
ClassLoader
ClassLoader中重要的方法是loadClass(String name),其他的子類都繼承了此方法且沒有進行復寫。
從上圖可以看出在加載類時首先判斷這個類是否之前被加載過,如果有則直接返回,如果沒有則首先嘗試讓parent ClassLoader進行加載,加載不成功才在自己的findClass中進行加載,這和java虛擬機中常見的雙親委派模型一致的。
BootClassLoader
和java虛擬機中不同的是BootClassLoader是ClassLoader內部類,由java代碼實現而不是c++實現,是Android平臺上所有ClassLoader的最終parent,這個內部類是包內可見,所以我們沒法使用。
URLClassLoader
只能用于加載jar文件,但是由于 dalvik 不能直接識別jar,所以在 Android 中無法使用這個加載器。
PathClassLoader
PathClassLoader是用來加載Android系統類和應用的類,只能加載系統中已經安裝過的apk,并且不建議開發者使用。
DexClassLoader
DexClassLoader支持加載APK、DEX和JAR,也可以從SD卡進行加載,就是通過這個實現動態加載技術,動態加載技術分為兩種,一種是動態加載So包,一種就是動態加載dex/jar/apk文件,安卓動態加載技術默認指的就是第二種方式,出于安全問題,Android并不允許直接加載手機外部存儲這類noexec(不可執行)存儲路徑上的可執行文件,都要先把他們拷貝到data/packagename/內部儲存文件路徑,確保庫不會被第三方應用惡意修改或攔截,然后再將他們加載到當前的運行環境并調用需要的方法執行相應的邏輯,從而實現動態調用。
上面說dalvik不能直接識別jar,DexClassLoader卻可以加載jar文件,這難道不矛盾嗎?其實在BaseDexClassLoader里對".jar",".zip",".apk",".dex"后綴的文件最后都會生成一個對應的dex文件,所以最終處理的還是dex文件,而URLClassLoader并沒有做類似的處理。
一般我們都是用這個DexClassLoader來作為動態加載的加載器。
注意:PathClassLoader只能加載已安裝的apk的dex,其實這說的應該是在dalvik虛擬機上,在art虛擬機上PathClassLoader可以加載未安裝的apk的dex(在art平臺上已驗證)。
ClassLoader加載class的過程
// BaseDexClassLoader
//創建
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
//加載
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
// PathClassLoader.java,optimizedDirectory永遠為null
//創建
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
// DexClassLoader.java
//創建
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
//DexPathList的findClass
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;
}
//DexFile的loadClassBinaryName
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
從上圖可以看到:
在創建類加載器過程
optimizedDirectory是用來緩存我們需要加載的dex文件的,并創建一個DexFile對象,optimizedDirectory必須是一個內部存儲路徑,DexClassLoader可以指定自己的optimizedDirectory,所以它可以加載外部的dex,因為這個dex會被復制到內部路徑的optimizedDirectory;而PathClassLoader沒有optimizedDirectory,所以它只能加載內部的dex,這些大都是存在系統中已經安裝過的apk里面的。
加載類的過程
BaseDexClassLoader中有個DexPathList實例pathList,pathList中包含一個DexFile的數組dexElements,dexElements數組就是這些dex文件的集合,dex文件就是內部存儲路徑optimizedDirectory緩存起來的。如果不分包一般這個數組只有一個Element元素,也就只有一個DexFile文件,而對于類加載呢,就是遍歷這個集合,通過DexFile去尋找。簡單來說就是:
1.在DexClassLoader的findClass 方法中通過一個DexPathList對象findClass()方法來獲取class
2.在DexPathList的findClass 方法中,對之前構造好dexElements數組集合進行遍歷,一旦找到類名與name相同的類時,就直接返回這個class,找不到則返回null。
動態加載技術
基于ClassLoader的動態加載技術的一個特點就是,如果程序不重新啟動,加載過一次的類就無法重新加載。因此,如果使用ClassLoader來動態升級APP或者動態修復BUG,都需要重新啟動APP才能生效。很容易可以想到熱修復實現的一種方式,假設現在代碼中的某一個類或者是某幾個類有bug,那么我們可以在修復完bug之后,將這些個類打包成一個補丁文件,然后通過這個補丁文件封裝出一個Element對象,并且將這個Element對象插到原有dexElements數組的最前端,這樣當DexClassLoader去加載類時,優先會從我們插入的這個Element中找到相應的類,雖然那個有bug的類還存在于數組中后面的Element中,但由于雙親加載機制的特點,這個有bug的類已經沒有機會被加載了,這樣一個bug就在沒有重新安裝應用的情況下修復了。微信的Tinker框架通過修復好的class.dex 和原有的class.dex比較差生差量包補丁文件patch.dex,在手機上這個patch.dex又會和原有的class.dex 合并生成新的文件fix_class.dex,用這個新的fix_class.dex 整體替換原有的dexPathList的中的內容。
除了微信的Tinker,其他的動態加載技術還可以使用jni hook的方式修改程序的執行代碼。前者是在虛擬機上操作的,而后者做的已經是Native層級的工作了,直接修改應用運行時的內存地址,所以使用jni hook的方式時,不用重新應用就能生效,比如阿里的dexposed和AndFix。
四.總結
以上就是關于類加載機制的知識點,如有不足或者錯誤的地方請指正。在技術這塊,我們需要多看更需要多寫,我們只有不斷學習,不斷進步才能不被淘汰。