什么是Android熱修復技術(shù)
簡單來說就是不重新安裝apk的情況下,通過補丁,修復bug


目前主流的熱修復技術(shù)框架
- 阿里系的: Andfix、Hotfix、Sophix
- 騰訊系的:QQ空間超級補丁技術(shù)、Qfix、Tinker(微信)
- 美團系的:Robust
- 餓了么的:Amigo
關(guān)于熱修復的技術(shù)積淀
- 最開始 ,是手淘基于Xposed進行了改進,產(chǎn)生了針對Android Dalvik虛擬機運行時的Java method Hook技術(shù)——Dexposed。但是這個方案由于對底層Dalvik結(jié)構(gòu)過于依賴,最終無法兼容Android5.0以后
- 后來支付寶提出了新的熱修復方案Andfix。Andfix同樣是一種底層結(jié)構(gòu)替換的方案,也達到了運行時生效及時修復的效果,阿里后來對Andfix改進,對相關(guān)業(yè)務(wù)解耦后,推出了阿里百川Hotfix方案,此時的修復已經(jīng)非常的不錯,對代碼修復需求都可以解決,而且全版本兼容,但是問題在于Anfix本身有局限,它只提供代碼層面的修復,對于資源和so庫的修復都還未能實現(xiàn)
- 最終在2017年Sophix的橫空出世,打破了各家熱修復技術(shù)紛爭的局面。在代碼修復,資源修復,so修復的方面,以及方案的安全性,易用性放慢,sophix都做到了業(yè)界領(lǐng)先
本文重點介紹如何在項目中實現(xiàn)代碼修復
通過類加載機制實現(xiàn)
- 優(yōu)點:適用性強、修復范圍廣、限制少
- 缺點:屬于熱修復中的冷修復、需要重啟App
通過底層替換方法實現(xiàn)
- 優(yōu)點:時效好、不需重啟,即使生效
- 缺點:受限制較多(需要修改虛擬機字段,如果手機廠商修改了虛擬機…….)
ClassLoader 簡介
對于 Java 程序來說,編寫程序就是編寫類,運行程序也就是運行類(編譯得到的 class 文件),其中起到關(guān)鍵作用的就是類加載器 ClassLoader。說起類加載器我就想到ClassLoader的雙親委托加載機制,接下來就介紹一下類加載的雙親機制
雙親機制
當類加載器收到加載類或資源的請求時,通常都是先委托給父類加載器加載,也就是說只有當父類加載器找不到指定類或資源時,自身才會執(zhí)行實際的類加載過程,具體的加載過程如下:
1 源 ClassLoader 先判斷該 Class 是否已加載,如果已加載,則直接返回 Class,如果沒有則委托給父類加載器。
2 父類加載器判斷是否加載過該 Class,如果已加載,則直接返回 Class,如果沒有則委托給祖父類加載器。
3 依此類推,直到始祖類加載器(引用類加載器)。
4 始祖類加載器判斷是否加載過該 Class,如果已加載,則直接返回 Class,如果沒有則嘗試從其對應(yīng)的類路徑下尋找 class 字節(jié)碼文件并載入。如果載入成功,則直接返回 Class,如果載入失敗,則委托給始祖類加載器的子類加載器。
5 始祖類加載器的子類加載器嘗試從其對應(yīng)的類路徑下尋找 class 字節(jié)碼文件并載入。如果載入成功,則直接返回 Class,如果載入失敗,則委托給始祖類加載器的孫類加載器。
6 依此類推,直到源 ClassLoader。
7 源 ClassLoader 嘗試從其對應(yīng)的類路徑下尋找 class 字節(jié)碼文件并載入。如果載入成功,則直接返回 Class,如果載入失敗,源 ClassLoader 不會再委托其子類加載器,而是拋出異常。
Android 中的ClassLoader
Android 的 Dalvik/ART 虛擬機如同標準 Java 的 JVM 虛擬機一樣,也是同樣需要加載 class 文件到內(nèi)存中來使用,但是在 ClassLoader 的加載細節(jié)上會有略微的差別。
Android的dex文件
Android 應(yīng)用打包成 apk 文件時,class 文件會被打包成一個或者多個 dex 文件,Android 中的 Dalvik/ART 無法像 JVM 那樣 直接 加載 class 文件和 jar 文件中的 class,需要通過 dx 工具來優(yōu)化轉(zhuǎn)換成 Dalvik byte code 才行,只能通過 dex 或者 包含 dex 的jar、apk 文件來加載(注意 odex 文件后綴可能是 .dex 或 .odex,也屬于 dex 文件),因此 Android 中的 ClassLoader 工作就交給了 BaseDexClassLoader 來處理。
如何通過類加載機制實現(xiàn)
首先需要認識BaseDexClassLoader、PathClassLoader和DexClassLoader
PathClassLoader:系統(tǒng)運作,app運行時用于加載app所有需要的類。PathClassLoader 里面除了這 2 個構(gòu)造方法以外就沒有其他的代碼了,具體的實現(xiàn)都是在 BaseDexClassLoader 里面,其 dexPath 比較受限制,一般是已經(jīng)安裝應(yīng)用的 apk 文件路徑
DexClassLoader:程序員運作,可以通過它加載我們想加載的資源,一般包括這么幾種:jar、dex、apk等。
BaseDexClassLoader:熱修復中的大Boss,PathClassLoader和DexClassLoader均繼承自BaseDexClassLoader,PathClassLoader和DexClassLoader的重要方法均在其父類BaseDexClassLoader中。(因此就需要從BaseDexClassLoader入手)。
對比 PathClassLoader 只能加載已經(jīng)安裝應(yīng)用的 dex 或 apk 文件,DexClassLoader 則沒有此限制,可以從 SD 卡上加載包含 class.dex 的 .jar 和 .apk 文件,這也是插件化和熱修復的基礎(chǔ),在不需要安裝應(yīng)用的情況下,完成需要使用的 dex 的加載。
BaseDexClassLoader查找類的源碼:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
通過源碼可以看到,BaseDexClassLoader通過pathList.findClass查找類的,這里出現(xiàn)一個 大Boss “PathList”
PathList:中保存類所有dex文件和信息,看一下它是怎么查找類的
PathList源碼:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
前方高能:
可以看到,PathList從dexElements中查找類,如果clazz != null直接return class,這就是我們可以利用的地方,從源碼看,dexElements應(yīng)該是個數(shù)組或者集合,設(shè)想:我們是不是可以把我們修復bug后的xx類,打包成dex,插入到dexElements的最前面,這樣,系統(tǒng)通過PathClassLoader,查找bug類的時候,就會下找到我們的修復bug的xx類,然后直接返回,不去管后面有bug的那個xx類,達到熱修復的功能
理一下我們熱修復的方案
修復有bug的類,生成dex補丁包;
通過反射機制得到PathClassLoader的成員變量PathList字段(DexPathList的屬性)(通過上面分析知道,PathList是PathClassLoader父類BaseDexClasLoader中的)
然后再反射PathList獲取它的dexElements字段(是一個存放dex的Element數(shù)組)
將我們生成的dex補丁包,插入到dexElements的數(shù)組的最前端
項目中的實現(xiàn)
實現(xiàn)步驟
編寫改變前的app
編寫熱修復需要重寫生成的類
生成dex補丁包,并放到服務(wù)器
編寫補丁檢測和下載代碼
編寫修復補丁代碼(即用反射拿到dexElements數(shù)組,把dex放到有問題的類之前)
如何生成dex補丁包
用class文件生成”001dex”補丁
android在sdk/build-tools/文件件下提供了”dx”命令工具,幫助我們將class文件生成dex文件
生成方式如下:
dx –dex –output=<要生成的文件> <’class’文件路徑>
例如:
dx –dex –output=001.dex …MainAtvity …Actvity2.class …People.class
核心代碼
/**
* 加載并安裝補丁
* @type {[type]}
*/
private void loadPatch(File file){
Log.d(TAG, file.getAbsolutePath()) ;
if(file.exists()){
Log.d(TAG,"文件存在...") ;
}else{
Log.d(TAG, "文件不存在...") ;
}
//獲取系統(tǒng)PathClassLoader
PathClassLoader pLoader = (PathClassLoader) context.getClassLoader();
//獲取PathClassLoader中的PathList
Object pPathList = getPathList(pLoader) ;
if(pPathList == null){
Log.d(TAG, "get PathClassLoader pathlist failed...") ;
return ;
}
//加載補丁
DexClassLoader dLoader = new DexClassLoader(file.getAbsolutePath(),optPath, null, pLoader) ;
//獲取DexClassLoader的pathLit,即BaseDexClassLoader中的pathList
Object dPathList = getPathList(dLoader) ;
if(dPathList == null){
Log.d(TAG, "get DexClassLoader pathList failed...") ;
return ;
}
//獲取PathList和DexClassLoader的DexElements
Object pElements = getElements(pPathList) ;
Object dElements = getElements(dPathList) ;
//將補丁dElements[]插入系統(tǒng)pElements[]的最前面
Object newElements = insertElements(pElements, dElements) ;
if(newElements == null){
Log.d(TAG, "patch insert failed...") ;
return ;
}
//用插入補丁后的新Elements[]替換系統(tǒng)Elements[]
try {
Field fElements = pPathList.getClass().getDeclaredField("dexElements") ;
fElements.setAccessible(true);
fElements.set(pPathList, newElements);
} catch (Exception e) {
e.printStackTrace();
Log.d(TAG, "fixed failed....") ;
return ;
}
}
/**
* 將補丁插入系統(tǒng)DexElements[]最前端,生成一個新的DexElements[]
* @param pElements
* @param dElements
* @return
*/
private Object insertElements(Object pElements, Object dElements){
//判斷是否為數(shù)組
if(pElements.getClass().isArray() && dElements.getClass().isArray()){
//獲取數(shù)組長度
int pLen = Array.getLength(pElements) ;
int dLen = Array.getLength(dElements) ;
//創(chuàng)建新數(shù)組
Object newElements = Array.newInstance(pElements.getClass().getComponentType(), pLen+dLen) ;
//循環(huán)插入
for(int i=0; i<pLen+dLen;i++){
if(i<dLen){
Array.set(newElements, i, Array.get(dElements, i));
}else{
Array.set(newElements, i, Array.get(pElements, i-dLen)) ;
}
}
return newElements ;
}
return null ;
}
/**
* 獲取DexElements
* @param object
* @return
*/
private Object getElements(Object object){
try {
Class<?> c = object.getClass() ;
Field fElements = c.getDeclaredField("dexElements") ;
fElements.setAccessible(true);
Object obj = fElements.get(object) ;
return obj ;
} catch (Exception e) {
e.printStackTrace();
}
return null ;
}
/**
* 通過反射機制獲取PathList
* @param loader
* @return
*/
private Object getPathList(BaseDexClassLoader loader){
try {
Class<?> c = Class.forName("dalvik.system.BaseDexClassLoader") ;
//獲取成員變量pathList
Field fPathList = c.getDeclaredField("pathList") ;
//抑制jvm檢測訪問權(quán)限
fPathList.setAccessible(true);
//獲取成員變量pathList的值
Object obj = fPathList.get(loader) ;
return obj ;
} catch (Exception e) {
e.printStackTrace();
}
return null ;
}