Android熱修復原理分析
本文目的是讓大家瞭解什麼是熱修復,具體的實現細節可以聯繫我。大家瞭解了原理後,才能夠順利的帶着手寫代碼了。請大家記住一句話:
紙上得來終覺淺,絕知此事要躬行。
代碼需要大家自己寫出來的那纔是自己的代碼,否則的話你看了,背了,最後工作中還是寫不出來優秀的代碼
什麼是熱修復
熱修復: 讓應用能夠在無需重新安裝的情況實現更新,幫助應用快速建立動態修復能力。
早期遇到Bug我們一般會緊急發佈了一個版本。然而這個Bug可能就是簡簡單單的一行代碼,爲了這一行代碼,進行全量或者增量更新迭代一個版本,未免有點大材小用了。而且新版本的普及需要時間,以Android用戶的升級習慣,即使是相對活躍的微信也需要10天以上的時間去覆蓋50%的用戶。使用熱修復技術,能做到1天覆蓋70%以上。這也是基於補丁體積較小,可以直接使用移動網絡下載更新。
熱修復開發流程:
目前Android業內,熱修復技術百花齊放,各大廠都推出了自己的熱修復方案,使用的技術方案也各有所異。其中QZone超級補丁基於的是dex分包方案,而dex分包是基於Java的類加載機制 ClassLoader
。
ClassLoader
介紹
任何一個 Java 程序都是由一個或多個 class 文件組成,在程序運行時,需要將 class 文件加載到虛擬機 中纔可以使用,負責加載這些 class 文件的就是 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
很多博客裏說
PathClassLoader
只能加載已安裝的apk
的dex
,但是實際上PathClassLoader
和DexClassLoader
一樣都能夠加載sdcard
中的dex
。
Log.e(TAG, "Activity.class 由:" + Activity.class.getClassLoader() +" 加載");
Log.e(TAG, "MainActivity.class 由:" + 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.enoyfix-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);
}
}
可以看到兩者唯一的區別在於: 創建 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
參數爲odex
的目錄。實際上Android中的ClassLoader
在加載dex
時,會首先經過dexopt
對dex
執行優化,產生odex
文件。optimizedDirectory
爲null時的默認路徑爲:/data/dalvik-cache
。並且處於安全考慮,此目錄需要使用app私有目錄,如:getCodeCacheDir()
在API 26源碼中,將DexClassLoader的optimizedDirectory標記爲了 deprecated 棄用,實現也變爲了:
javapublicDexClassLoader(StringdexPath,StringoptimizedDirectory,StringlibrarySearchPath,ClassLoaderparent){super(dexPath,null,librarySearchPath,parent);}
和
PathClassLoader
一摸一樣了!
雙親委託機制
創建 ClassLoader
需要接收一個 ClassLoaderparent
參數。這個 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
:newPathClassLoader("/sdcard/xx.dex",getClassLoader());
並不僅僅只能獲得xx.dex
中的Class,還能夠獲得其父ClassLoader
中加載的Class。
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, FileoptimizedDirectory,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個元素。
而對於類的查找,由代碼for(Elementelement:dexElements)
得知,會由數組從前往後進行查找。
在 PathClassLoader
中的Element數組爲:[patch.dex
, classes.dex
, classes2.dex
]。如果存在Key.class位於patch.dex
與classes2.dex
中都存在一份,當進行類查找時,循環獲得 dexElements
中的 DexFile
,查找到了Key.class則立即返回,不會再管後續的element中的 DexFile
是否能加載到Key.class了。
因此,可以將出現Bug的class單獨的製作一份patch.dex
文件(補丁包),然後在程序啓動時,從服務器下載patch.dex
保存到某個路徑,再通過patch.dex
的文件路徑,用其創建 Element對象,然後將這個 Element對象插入到我們程序的類加載器 PathClassLoader
的 pathList
中的 dexElements
數組頭部。這樣在加載出現Bug的class時會優先加載patch.dex
中的修復類,從而解決Bug。QQ空間熱修復的原理就是這樣,利用反射Hook了PathClassLoader
中pathList
的dexElements
數組。
總結
在實現熱修復的過程中,必須掌握的技術包括了反射、類加載機制並且掌握Framewrok
層源碼中關於 ClassLoader
部分的內容。同時如果需要繼續自動化補丁生成還需要掌握gradle
編程等內容。
文章中每一個部分都包含一系列BAT面試的面試點,這些點構建了一個完整的知識體系,熱修復。後面,我會細化裏面的知識,如果 大家覺得有問題,可以和我交流~