Android動態載入ClassLoader
ClassLoader工作機制(參考http://www.cnblogs.com/xujian2014/p/5551153.html)
一、ClassLoader概念
ClassLoader是用來動態的載入class檔案到虛擬機器中,並轉換成java.lang.class類的一個例項,每個這樣的例項用來表示一個java類,我們可以根據Class的例項得到該類的資訊,並通過例項的newInstance()方法創建出該類的一個物件,除此之外,ClassLoader還負責載入Java應用所需的資源,如影象檔案和配置檔案等。
ClassLoader類是一個抽象類。如果給定類的二進位制名稱,那麼類載入器會試圖查詢或生成構成類定義的資料。一般策略是將名稱轉換為某個檔名,然後從檔案系統讀取該名稱的“類檔案”。ClassLoader類使用委託模型來搜尋類和資源。每個 ClassLoader例項都有一個相關的父類載入器。需要查詢類或資源時,ClassLoader例項會在試圖親自查詢類或資源之前,將搜尋類或資源的任務委託給其父類載入器。
注意:程式在啟動的時候,並不會一次性載入程式所要用的所有class檔案,而是根據程式的需要,通過Java的類載入機制來動態載入某個class檔案到記憶體中。
二、JVM平臺提供三層classLoader
- Bootstrap classLoader:採用native code實現,是JVM的一部分,主要載入JVM自身工作需要的類,如java.lang.*、java.uti.*等; 這些類位於$JAVA_HOME/jre/lib/rt.jar。Bootstrap ClassLoader不繼承自ClassLoader,因為它不是一個普通的Java類,底層由C++編寫,已嵌入到了JVM核心當中,當JVM啟動後,Bootstrap ClassLoader也隨著啟動,負責載入完核心類庫後,並構造Extension ClassLoader和App ClassLoader類載入器。
- ExtClassLoader:擴充套件的class loader,載入位於$JAVA_HOME/jre/lib/ext目錄下的擴充套件jar。
- AppClassLoader:系統class loader,父類是ExtClassLoader,載入$CLASSPATH下的目錄和jar;它負責載入應用程式主函式類。
其體系結構圖如下:
如果要實現自己的類載入器,不管是實現抽象列ClassLoader,還是繼承URLClassLoader類,它的父載入器都是AppClassLoader,因為不管呼叫哪個父類載入器,建立的物件都必須最終呼叫getSystemClassLoader()作為父載入器,getSystemClassLoader()方法獲取到的正是AppClassLoader。
注意:Bootstrap classLoader並不屬於JVM的等級層次,它不遵守ClassLoader的載入規則,Bootstrap classLoader並沒有子類。
三、JVM載入class檔案到記憶體有兩種方式
- 隱式載入:不通過在程式碼裡呼叫ClassLoader來載入需要的類,而是通過JVM來自動載入需要的類到記憶體,例如:當類中繼承或者引用某個類時,JVM在解析當前這個類不在記憶體中時,就會自動將這些類載入到記憶體中。
- 顯示載入:在程式碼中通過ClassLoader類來載入一個類,例如呼叫this.getClass.getClassLoader().loadClass()或者Class.forName()。
四、ClassLoader載入類的過程
- 找到.class檔案並把這個檔案載入到記憶體中
- 位元組碼驗證,Class類資料結構分析,記憶體分配和符號表的連結
- 類中靜態屬性和初始化賦值以及靜態程式碼塊的執行
五、自定義類載入器
1、為何要自定義類載入器?
JVM提供的類載入器,只能載入指定目錄的jar和class,如果我們想載入其他位置的類或jar時,例如載入網路上的一個class檔案,預設的ClassLoader就不能滿足我們的需求了,所以需要定義自己的類載入器。
2、如何實現自定義的類載入器?
我們實現一個ClassLoader,並指定這個ClassLoader的載入路徑。有兩種方式:
方式一:繼承ClassLoader,重寫父類的findClass()方法,程式碼:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class PathClassLoader extends ClassLoader
{
public static final String drive = "d:/";
public static final String fileType = ".class";
public static void main(String[] args) throws Exception
{
PathClassLoader loader = new PathClassLoader();
Class<?> objClass = loader.loadClass("HelloWorld", true);
Object obj = objClass.newInstance();
System.out.println(objClass.getName());
System.out.println(objClass.getClassLoader());
System.out.println(obj.getClass().toString());
}
public Class<?> findClass(String name)
{
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);// 將一個 byte 陣列轉換為 Class// 類的例項
}
public byte[] loadClassData(String name)
{
FileInputStream fis = null;
byte[] data = null;
try
{
fis = new FileInputStream(new File(drive + name + fileType));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int ch = 0;
while ((ch = fis.read()) != -1)
{
baos.write(ch);
}
data = baos.toByteArray();
} catch (IOException e)
{
e.printStackTrace();
}
return data;
}
}
在第13行,我們呼叫了父類的loadClass()方法,該方法使用指定的二進位制名稱來載入類,下面是loadClass方法的原始碼:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name))
{
// 第一步先檢查這個類是否已經被載入
Class<?> c = findLoadedClass(name);
if (c == null)
{
long t0 = System.nanoTime();
try
{
//parent為父載入器
if (parent != null)
{
//將搜尋類或資源的任務委託給其父類載入器
c = parent.loadClass(name, false);
} else
{
//檢查該class是否被BootstrapClassLoader載入
c = findBootstrapClassOrNull(name);
}
}
catch (ClassNotFoundException e)
{
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null)
{
//如果上述兩步均沒有找到載入的class,則呼叫findClass()方法
long t1 = System.nanoTime();
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;
}
}
這個方法首先檢查指定class是否已經被載入,如果已被載入過,則呼叫resolveClass()方法連結指定的類,如果還未載入,則先將搜尋類或資源的任務委託給其父類載入器,檢查該class是否被BootstrapClassLoader載入,如果上述兩步均沒有找到載入的class,則呼叫findClass()方法,在我們自定義的載入器中,我們重寫了findClass方法,去我們指定的路徑下載入class檔案。
另外,我們自定義的類載入器沒有指定父載入器,在JVM規範中不指定父類載入器的情況下,預設採用系統類載入器即AppClassLoader作為其父載入器,所以在使用該自定義類載入器時,需要載入的類不能在類路徑中,否則的話根據雙親委派模型的原則,待載入的類會由系統類載入器載入。如果一定想要把自定義載入器需要載入的類放在類路徑中, 就要把自定義類載入器的父載入器設定為null。
方式二:繼承URLClassLoader類,然後設定自定義路徑的URL來載入URL下的類。
我們將指定的目錄轉換為URL路徑,然後重寫findClass方法。
六、實現類的熱部署
1、什麼是類的熱部署?
所謂熱部署,就是在應用正在執行的時候升級軟體,不需要重新啟用應用。
對於Java應用程式來說,熱部署就是執行時更新Java類檔案。在基於Java的應用伺服器實現熱部署的過程中,類裝入器扮演著重要的角色。大多數基於Java的應用伺服器,包括EJB伺服器和Servlet容器,都支援熱部署。
類裝入器不能重新裝入一個已經裝入的類,但只要使用一個新的類裝入器例項,就可以將類再次裝入一個正在執行的應用程式。
2、如何實現Java類的熱部署
前面的分析,我們已經知道,JVM在載入類之前會檢查請求的類是否已經被載入過來,也就是要呼叫findLoadedClass方法檢視是否能夠返回類例項。如果類已經載入過來,再呼叫loadClass會導致類衝突。
但是,JVM判斷一個類是否是同一個類有兩個條件:一是看這個類的完整類名是否一樣(包括包名),二是看載入這個類的ClassLoader載入器是否是同一個(既是是同一個ClassLoader類的兩個例項,載入同一個類也會不一樣)。
所以,要實現類的熱部署可以建立不同的ClassLoader的例項物件,然後通過這個不同的例項物件來載入同名的類。
Android動態載入之ClassLoader詳解(參考http://www.jianshu.com/p/a620e368389a)
Dalvik虛擬機器如同其他Java虛擬機器一樣,在執行程式時首先需要將對應的類載入到記憶體中。而在Java標準的虛擬機器中,類載入可以從class檔案中讀取,也可以是其他形式的二進位制流。因此,我們常常利用這一點,在程式執行時手動載入Class,從而達到程式碼動態載入執行的目的。
只不過Android平臺上虛擬機器執行的是Dex位元組碼,一種對class檔案優化的產物,傳統Class檔案是一個Java原始碼檔案會生成一個.class檔案,而Android是把所有Class檔案進行合併,優化,然後生成一個最終的class.dex,目的是把不同class檔案重複的東西只需保留一份,如果我們的Android應用不進行分dex處理,最後一個應用的apk只會有一個dex檔案。
Android平臺的ClassLoader
Android中的ClassLoader
Android中類載入器有BootClassLoader,URLClassLoader,
PathClassLoader,DexClassLoader,BaseDexClassLoader,等都最終繼承自java.lang.ClassLoader。
-
ClassLoader
java.lang.ClassLoader是所有ClassLoader的最終父類。構造方法主要以下兩種
1.傳入一個父類構造器
實際構造器 2.無參預設構造法
無參構造器 可以看出ClassLoader主要就是傳入一個父構造器,而且一般父構造器不能為空,不像java虛擬機器裡父構造器為空時預設的父構造器為Bootstrap ClassLoader。Android中預設無父構造器傳入的情況下,預設父構造器為一個PathClassLoader且此PathClassLoader父構造器為BootClassLoader。
ClassLoader中重要的方法是loadClass(String name),其他的子類都繼承了此方法且沒有進行復寫。
laodClass.png 可以看出在載入類時首先判斷這個類是否之前被載入過,如果有則直接返回,如果沒有則首先嚐試讓parent ClassLoader進行載入,載入不成功才在自己的findClass中進行載入。這和java虛擬機器中常見的雙親委派模型一致的,這種模型並不是一個強制性的約束模型,比如你可以繼承ClassLoader複寫loadCalss方法來破壞這種模型,只不過雙親委派模是一種被推薦的實現類載入器的方式,而且jdk1.2以後已經不提倡使用者在覆蓋loadClass方法,而應該把自己的類載入邏輯寫到findClass中。 -
BootClassLoader
和java虛擬機器中不同的是BootClassLoader是ClassLoader內部類,由java程式碼實現而不是c++實現,是Android平臺上所有ClassLoader的最終parent,這個內部類是包內可見,所以我們沒法使用。 -
URLClassLoader
只能用於載入jar檔案,但是由於 dalvik 不能直接識別jar,所以在 Android 中無法使用這個載入器。 -
BaseDexClassLoader
PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader,其中的主要邏輯都是在BaseDexClassLoader完成的。這些原始碼在java/dalvik/system中。
先看下BaseDexClassLoader的構造方式:
BaseDexClassLoader.png
BaseDexClassLoader的建構函式包含四個引數,分別為:- dexPath,指目標類所在的APK或jar檔案的路徑,類裝載器將從該路徑中尋找指定的目標類,該類必須是APK或jar的全路徑.如果要包含多個路徑,路徑之間必須使用特定的分割符分隔,特定的分割符可以使用System.getProperty(“path.separtor”)獲得。上面"支援載入APK、DEX和JAR,也可以從SD卡進行載入"指的就是這個路徑,最終做的是將dexPath路徑上的檔案ODEX優化到內部位置optimizedDirectory,然後,再進行載入的。
- File optimizedDirectory,由於dex檔案被包含在APK或者Jar檔案中,因此在裝載目標類之前需要先從APK或Jar檔案中解壓出dex檔案,該引數就是制定解壓出的dex 檔案存放的路徑。這也是對apk中dex根據平臺進行ODEX優化的過程。其實APK是一個程式壓縮包,裡面包含dex檔案,ODEX優化就是把包裡面的執行程式提取出來,就變成ODEX檔案,因為你提取出來了,系統第一次啟動的時候就不用去解壓程式壓縮包的程式,少了一個解壓的過程。這樣的話系統啟動就加快了。為什麼說是第一次呢?是因為DEX版本的也只有第一次會解壓執行程式到 /data/dalvik-cache(針對PathClassLoader)或者optimizedDirectory(針對DexClassLoader)目錄,之後也是直接讀取目錄下的的dex檔案,所以第二次啟動就和正常的差不多了。當然這只是簡單的理解,實際生成的ODEX還有一定的優化作用。ClassLoader只能載入內部儲存路徑中的dex檔案,所以這個路徑必須為內部路徑。
- libPath,指目標類中所使用的C/C++庫存放的路徑
- classload,是指該裝載器的父裝載器,一般為當前執行類的裝載器,例如在Android中以context.getClassLoader()作為父裝載器。
-
DexClassLoader
在看下DexClassLoader和PathClassLoader的構造器:
DexClassLoader DexClassLoader支援載入APK、DEX和JAR,也可以從SD卡進行載入。
上面說dalvik不能直接識別jar,DexClassLoader卻可以載入jar檔案,這難道不矛盾嗎?其實在BaseDexClassLoader裡對".jar",".zip",".apk",".dex"字尾的檔案最後都會生成一個對應的dex檔案,所以最終處理的還是dex檔案,而URLClassLoader並沒有做類似的處理。
一般我們都是用這個DexClassLoader來作為動態載入的載入器。 -
PathClassLoader
PathClassLoader
optimized_dexFilepath
很簡單明瞭,可以看出PathClassLoader沒有將optimizedDirectory置為Null,也就是沒設定優化後的存放路徑。其實optimizedDirectory為null時的預設路徑就是/data/dalvik-cache 目錄。
PathClassLoader是用來載入Android系統類和應用的類,並且不建議開發者使用。
advice 很多部落格裡說PathClassLoader只能載入已安裝的apk的dex,其實這說的應該是在dalvik虛擬機器上,在art虛擬機器上PathClassLoader可以載入未安裝的apk的dex(在art平臺上已驗證),然而在/data/dalvik-cache 確未找到相應的dex檔案,懷疑是art虛擬機器判斷apk未安裝,所以只是將apk優化後的odex放在記憶體中,之後進行釋放,這只是個猜想,希望有知道的可以告知一下。因為dalvik上無法使用,所以我們也沒法使用。
ClassLoader載入class的過程
#BaseDexClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
#DexPathList
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
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
可以看出,BaseDexClassLoader中有個pathList物件,pathList中包含一個DexFile的陣列dexElements,由上面分析知道,dexPath傳入的原始dex(.apk,.zip,.jar等)檔案在optimizedDirectory資料夾中生成相應的優化後的odex檔案,dexElements陣列就是這些odex檔案的集合,如果不分包一般這個陣列只有一個Element元素,也就只有一個DexFile檔案,而對於類載入呢,就是遍歷這個集合,通過DexFile去尋找。最終呼叫native方法的defineClass。
ART虛擬機器的相容性問題
Android Runtime(縮寫為ART),在Android 5.0及後續Android版本中作為正式的執行時庫取代了以往的Dalvik虛擬機器。ART能夠把應用程式的位元組碼轉換為機器碼,是Android所使用的一種新的虛擬機器。它與Dalvik的主要不同在於:Dalvik採用的是JIT技術,位元組碼都需要通過即時編譯器(just
in time ,JIT)轉換為機器碼,這會拖慢應用的執行效率,而ART採用Ahead-of-time(AOT)技術,應用在第一次安裝的時候,位元組碼就會預先編譯成機器碼,這個過程叫做預編譯。ART同時也改善了效能、垃圾回收(Garbage
Collection)、應用程式除錯以及效能分析。但是請注意,執行時記憶體佔用空間較少同樣意味著編譯二進位制需要更高的儲存。
ART模式相比原來的Dalvik,會在安裝APK的時候,使用Android系統自帶的dex2oat工具把APK裡面的.dex檔案轉化成OAT檔案,OAT檔案是一種Android私有ELF檔案格式,它不僅包含有從DEX檔案翻譯而來的本地機器指令,還包含有原來的DEX檔案內容。這使得我們無需重新編譯原有的APK就可以讓它正常地在ART裡面執行,也就是我們不需要改變原來的APK程式設計介面。ART模式的系統裡,同樣存在DexClassLoader類,包名路徑也沒變,只不過它的具體實現與原來的有所不同,但是介面是一致的。實際上,ART執行時就是和Dalvik虛擬機器一樣,實現了一套完全相容Java虛擬機器的介面