目錄
一 前言介紹
從16年開始開始,熱修復技術開始在安卓界流行,他不用發佈新版本就可以修復線上 bug ,讓已經發行的線上版本有能力去進行全量或者增量更新,讓開發者們不用爲了某一些小bug大費周章地迭代一個新版本。
學習熱修復技術是安卓開發者進階必不可少的一條道路之一,而熱修復的核心就是:classloader類加載機制。
值得注意的一個前提是,java的ClassLoader和安卓的ClassLoader不盡相同,安卓是自己實現的 ART 或者 Dalvik 虛擬機,這篇主要講安卓中的ClassLoader。
1.1 ART 和 Dalvik
Dalvik是Google公司自己設計用於Android平臺的Java虛擬機。支持已轉換爲.dex(Dalvik Executable)格式的Java應用程序的運行。DVM默認使用CMS垃圾回收器,與JVM運行 Class 字節碼不同的是,DVM 執行 Dex(Dalvik Executable Format) ——專爲 Dalvik 設計的一種壓縮格式,適合於內存和處理器速度有限的系統。Dex 文件是很多 .class 文件處理壓縮後的產物,最終可以在 Android 運行時環境執行。
ART(Android Runtime) 在 Android 4.4 中引入,在 Android 5.0 及更高版本作爲默認的 Android 運行時。ART做出的具體改進可看安卓官方文檔介紹:運行時:Android Runtime (ART) 和 Dalvik。在應用安裝的時候Ahead-Of-Time(AOT)預編譯字節碼到機器語言,這一機制叫Ahead-Of-Time(AOT)預編譯。應用程序安裝會變慢,但是執行將更有效率,啓動更快。
ART 和 Dalvik 都是運行 Dex 字節碼的兼容運行時,因此 ART 向下兼容Dalvik 開發的應用。
1.2 dexopt與dexaot
-
dexopt
在Dalvik虛擬機加載一個dex文件時,會對 dex 文件進行驗證和優化,得到odex(Optimized dex) 文件。這個文件和 dex 文件很像,只是使用了一些優化操作碼。
-
dex2oat
ART 預先編譯機制,在安裝時對 dex 文件執行dexopt優化之後,再將odex進行 AOT 提前編譯操作,編譯爲OAT(實際上是ELF文件)可執行文件(機器碼)。(相比做過odex優化,未做過優化的dex轉換成OAT要花費更長的時間)
1.3 ART 和 Dalvik 對比
1、在Dalvik下,應用運行需要解釋執行,常用熱點代碼通過即時編譯器(JIT)將字節碼轉換爲機器碼,運行效率低。而在ART 環境中,應用在安裝時,字節碼預編譯(AOT)成機器碼,安裝慢了,但運行效率會提高。
2、ART佔用空間比Dalvik大(字節碼變爲機器碼), “空間換時間"。
3、預編譯也可以明顯改善電池續航,因爲應用程序每次運行時不用重複編譯了,從而減少了 CPU 的使用頻率,降低了能耗。
二 ClassLoader
2.1 基本介紹
任何一個 Java 程序都是由一個或多個 class 文件組成,在程序運行時,需要將 class 文件加載到 JVM 中纔可以使用,負責加載這些 class 文件的就是 Java 的類加載機制。
但Java程序啓動時,並不是一次性加載所有類然後運行,而是先把保證運行的基礎類加載到jvm,其它類要用時再加載。這樣的好處是節省了內存的開銷(因爲java最早爲嵌入式系統而設計的,內存寶貴)。用到時再加載這也是java動態性的一種體現。
ClassLoader 的作用簡單來說就是加載 class 文件,提供給程序運行時使用。每個 Class 對象的內部都有一個 classLoader 字段來標識自己是由哪個 ClassLoader 加載的。
class Class<T> {
...
private transient ClassLoader classLoader;
...
}
ClassLoader是一個抽象類,而它的具體實現類主要有:
BootClassLoader |
用於加載Android Framework層class文件 |
PathClassLoader |
用於Android應用程序類加載器。可以加載指定的dex,以及jar、zip、apk中的classes.dex |
DexClassLoader |
用於加載指定的dex,以及jar、zip、apk中的classes.dex |
Log.e(TAG, "Activity.class 由:" + Activity.class.getClassLoader() +" 加載");
Log.e(TAG, "MainActivity.class 由:" + getClassLoader() +" 加載");
//輸出:
Activity.class 由:java.lang.BootClassLoader@b1202a1 加載
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]]] 加載
它們之間的關係如下:
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);
}
}
可以看到兩者唯一的區別在於:創建DexClassLoader
需要傳遞一個optimizedDirectory
參數,並且會將其創建爲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());
其實optimizedDirectory
參數就是dexopt的產出目錄(odex)。那PathClassLoader
創建時,這個目錄爲null,就意味着不進行dexopt?並不是,optimizedDirectory
爲null時的默認路徑爲:/data/dalvik-cache。
在API 26源碼中,將DexClassLoader的optimizedDirectory標記爲了 deprecated 棄用,實現也變得和PathClassLoader一摸一樣了:
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
2.2 雙親委託機制
可以看到創建ClassLoader
需要接收一個ClassLoader parent
參數。這個parent
的目的就在於實現類加載的雙親委託。即:某個類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
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,則調用parent的loadClass進行加載
c = parent.loadClass(name, false);
} else {
//parent爲null,則調用BootClassLoader進行加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果都找不到就自己查找
long t1 = System.nanoTime();
c = findClass(name);
}
}
return c;
}
因此我們自己創建的ClassLoader: new PathClassLoader("/sdcard/xx.dex", getClassLoader());
並不僅僅只能加載 xx.dex中的class。
值得注意的是:c = findBootstrapClassOrNull(name);
按照方法名理解,應該是當parent爲null時候,也能夠加載BootClassLoader
加載的類。但是實際上,Android當中的實現爲:(Java不同)
private Class findBootstrapClassOrNull(String name)
{
return null;
}
2.3 類加載器的三個機制(約束)
委託:如上所述,委託機制是指將加載一個類的請求交給父類加載器,如果這個父類加載器不能夠找到或者加載這個類,那麼再加載它。
可見性:可見性的原理是子類的加載器可以看見所有的父類加載器加載的類,而父類加載器看不到子類加載器加載的類。
單一性:單一性原理是指僅加載一個類一次,這是由委託機制確保子類加載器不會再次加載父類加載器加載過的類。
2.4 findClass
可以看到在所有父ClassLoader無法加載Class時,則會調用自己的findClass
方法。findClass
在ClassLoader中的定義爲:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
其實任何ClassLoader子類,都可以重寫loadClass
與findClass
。一般如果你不想使用雙親委託,則重寫loadClass
修改其實現。而重寫findClass
則表示在雙親委託下,父ClassLoader都找不到Class的情況下,定義自己如何去查找一個Class。而我們的PathClassLoader
會自己負責加載MainActivity
這樣的程序中自己編寫的類,利用雙親委託父ClassLoader加載Framework中的Activity
。說明PathClassLoader
並沒有重寫loadClass
,因此我們可以來看看PathClassLoader中的 findClass
是如何實現的。
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);
}
throw cnfe;
}
return c;
}
實現非常簡單,從pathList
中查找class。繼續查看DexPathList:
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
//.........
// splitDexPath 實現爲返回 List<File>.add(dexPath)
// makeDexElements 會去 List<File>.add(dexPath) 中使用DexFile加載dex文件返回 Element數組
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
//.........
}
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;
}
三 熱修復
PathClassLoader
中存在一個Element數組,Element類中存在一個dexFile成員表示dex文件,即:APK中有X個dex,則Element數組就有X個元素。
在PathClassLoader
中的Element數組爲:[patch.dex , classes.dex , classes2.dex]。如果存在Key.class位於patch.dex與classes2.dex中都存在一份,當進行類查找時,循環獲得dexElements
中的DexFile,查找到了Key.class則立即返回,不會再管後續的element中的DexFile是否能加載到Key.class了。
因此實際上,一種熱修復實現可以將出現Bug的class單獨的製作一份fix.dex文件(補丁包),然後在程序啓動時,從服務器下載fix.dex保存到某個路徑,再通過fix.dex的文件路徑,用其創建Element
對象,然後將這個Element
對象插入到我們程序的類加載器PathClassLoader
的pathList
中的dexElements
數組頭部。這樣在加載出現Bug的class時會優先加載fix.dex中的修復類,從而解決Bug。
熱修復的方式不止這一種,並且如果要完整實現此種熱修復可能還需要注意一些其他的問題(如:反射兼容)。
參考文章:
Android Runtime (ART) 和 Dalvik