1. 程式人生 > >【新技能get】讓App像Web一樣釋出新版本

【新技能get】讓App像Web一樣釋出新版本

背景

當一個App釋出之後,突然發現了一個嚴重bug需要進行緊急修復,這時候公司各方就會忙得焦頭爛額:重新打包App、測試、向各個應用市場和渠道換包、提示使用者升級、使用者下載、覆蓋安裝。有時候僅僅是為了修改了一行程式碼,也要付出巨大的成本進行換包和重新發布。這時候就提出一個問題:有沒有辦法以補丁的方式動態修復緊急Bug,不再需要重新發布App,不再需要使用者重新下載,覆蓋安裝?雖然Android系統並沒有提供這個技術,但是很幸運的告訴大家,答案是:可以。

解決方案

該方案基於的是android dex分包方案的,關於dex分包方案,網上有幾篇解釋了,所以這裡就不再贅述,具體可以看這裡:https://m.oschina.net/blog/308583(請複製連結到瀏覽器開啟)。

簡單的概括一下,就是把多個dex檔案塞入到app的classloader之中,但是android dex拆包方案中的類是沒有重複的,如果classes.dex和classes1.dex中有重複的類,當用到這個重複的類的時候,系統會選擇哪個類進行載入呢?

讓我們來看看類載入的程式碼:

public Class findClass(String name, List<Throwable> suppressed) {  

for (Element element : dexElements) { //每個Element就是一個dex檔案

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
;

}

一個ClassLoader可以包含多個dex檔案,每個dex檔案是一個Element,多個dex檔案排列成一個有序的陣列dexElements,當找類的時候,會按順序遍歷dex檔案,然後從當前遍歷的dex檔案中找類,如果找類則返回,如果找不到從下一個dex檔案繼續查詢。、理論上,如果在不同的dex中有相同的類存在,那麼會優先選擇排在前面的dex檔案的類,如下圖:


在此基礎上,我們構想了熱補丁的方案,把有問題的類打包到一個dex(patch.dex)中去,然後把這個dex插入到Elements的最前面,如下圖


好,該方案基於第二個拆分dex的方案,方案實現如果懂拆分dex的原理的話,大家應該很快就會實現該方案,如果沒有拆分dex的專案的話,可以參考一下谷歌的multidex方案實現。然後在插入陣列的時候,把補丁包插入到最前面去。

好,看似問題很簡單,輕鬆的搞定了,讓我們來試驗一下,修改某個類,然後打包成dex,插入到classloader,當載入類的時候出現了(本例中是ActivityManager要被替換):


為什麼會出現以上問題呢?

從log的意思上來講,ModuleManager引用了ActivityManager,但是發現這這兩個類所在的dex不在一起,其中:

1. ModuleManager在classes.dex中

2. ActivityManager在patch.dex中

結果發生了錯誤。

這裡有個問題,拆分dex的很多類都不是在同一個dex內的,怎麼沒有問題?

讓我們搜尋一下丟擲錯誤的程式碼所在,嘿咻嘿咻,找到了一下程式碼:


從程式碼上來看,如果兩個相關聯的類在不同的dex中就會報錯,但是拆分dex沒有報錯這是為什麼,原來這個校驗的前提是:

如果引用者(也就是ModuleManager)這個類被打上了CLASS_ISPREVERIFIED標誌那麼就會進行dex的校驗。那麼這個標誌是什麼時候被打上去的?

讓我們在繼續搜尋一下程式碼,嘿咻嘿咻~~,在DexPrepare.cpp找到了一下程式碼:


這段程式碼是dex轉化成odex(dexopt)的程式碼中的一段,我們知道當一個apk在安裝的時候,apk中的classes.dex會被虛擬機器(dexopt)優化成odex檔案,然後才會拿去執行.

虛擬機器在啟動的時候,會有許多的啟動引數,其中一項就是verify選項,當verify選項被開啟的時候,上面doVerify變數為true,那麼就會執行dvmVerifyClass進行類的校驗,如果dvmVerifyClass校驗類成功,那麼這個類會被打上CLASS_ISPREVERIFIED的標誌,那麼具體的校驗過程是什麼樣子的呢?

此程式碼在DexVerify.cpp中,如下:


驗證clazz->directMethods方法,directMethods包含了以下方法:

1. static方法

2. private方法

3. 建構函式

2. clazz->virtualMethods

虛擬函式=override方法?

概括一下就是如果以上方法中直接引用到的類(第一層級關係,不會進行遞迴搜尋)和clazz都在同一個dex中的話,那麼這個類就會被打上CLASS_ISPREVERIFIED標誌


所以為了實現補丁方案,所以必須從這些方法中入手,防止類被打上CLASS_ISPREVERIFIED標誌。

最終空間的方案是往所有類的建構函式裡面插入了一段程式碼,程式碼如下:

if (ClassVerifier.PREVENT_VERIFY) {

System.out.println(AntilazyLoad.class);

}


其中AntilazyLoad類會被打包成單獨的hack.dex,這樣當安裝apk的時候,classes.dex內的類都會引用一個在不相同dex中的AntilazyLoad類這樣就防止了類被打上CLASS_ISPREVERIFIED的標誌了。只要沒被打上這個標誌的類都可以進行打補丁操作。然後在應用啟動的時候載入進來.AntilazyLoad類所在的dex包必須被先載入進來,不然AntilazyLoad類會被標記為不存在,即使後續載入了hack.dex包,那麼他也是不存在的,這樣螢幕就會出現茫茫多的類AntilazyLoad找不到的log。

所以Application作為應用的入口不能插入這段程式碼。(因為載入hack.dex的程式碼是在Application中onCreate中執行的,如果在Application的建構函式裡面插入了這段程式碼,那麼就是在hack.dex載入之前就使用該類,該類一次找不到,會被永遠的打上找不到的標誌)。

其中:

class ClassVerifier {

public static boolean PREVENT_VERIFY = false;//false防止程式碼被執行,提高效能

}

之所以選擇建構函式是因為他不增加方法數,一個類即使沒有顯式的建構函式,也會有一個隱式的預設建構函式。

空間使用的是在位元組碼插入程式碼,而不是原始碼插入,使用的是javaassist庫來進行位元組碼插入的。

隱患

虛擬機器在安裝期間為類打上CLASS_ISPREVERIFIED標誌是為了提高效能的,我們強制防止類被打上標誌是否會影響效能?這裡我們會做一下更加詳細的效能測試。

但是在大專案中拆分dex的問題已經比較嚴重,很多類都沒有被打上這個標誌。

如何打包補丁包:

1.空間在正式版本釋出的時候,會生成一份快取檔案,裡面記錄了所有class檔案的md5。還有一份mapping混淆檔案。

2.在後續的版本中使用-applymapping選項,應用正式版本的mapping檔案,然後計算編譯完成後的class檔案的md5和正式版本進行比較,把不相同的class檔案打包成補丁包。

備註:該方案現在也應用到我們的編譯過程當中,編譯不需要重新打包dex,只需要把修改過的類的class檔案打包成patch dex,然後放到sdcard下,那麼就會讓改變的程式碼生效。