Android 手寫熱修復dex
現有的熱修復框架很多,尤以AndFix 和Tinker比較多
具體的實現方式和項目引用可以參考網絡上的文章,今天就不談,也不是主要目的
今天就來探討,如何手寫一個熱修復的功能
對於簡單的項目,不想集成其他修復框架的SDK,也不想用第三方平台,只是緊急修復一些bug 還是挺方便的
言歸正傳,如果一個或多個類出現bug,導致了崩潰或者數據顯示異常,如果修復呢,如果熟悉jvm dalvik 類的加載機制,就會清楚的瞭解 ClassLoader的 雙親委託機制 就可以通過這個
什麼是雙親委託機制
- 當前ClassLoader首先從自己已經加載的類中查詢是否此類已經加載,如果已經加載則直接返回原來已經加載的類。 每個類加載器都有自己的加載緩存,當一個類被加載了以後就會放入緩存,等下次加載的時候就可以直接返回了。
- 當前classLoader的緩存中沒有找到被加載的類的時候,委託父類加載器去加載,父類加載器採用同樣的策略,首先查看自己的緩存,然後委託父類的父類去加載,一直到bootstrp ClassLoader.
- 當所有的父類加載器都沒有加載的時候,再由當前的類加載器加載,並將其放入它自己的緩存中,以便下次有加載請求的時候直接返回。
突破口來了,看1(如果已經加載則直接返回原來已經加載的類) 對於同一個類,如果先加載修復的類,當後續在加載未修復的類的時候,直接返回修復的類,這樣bug不就解決了嗎?
Nice ,多看源碼和jvm 許多問題可以從framework和底層去解決
話不多説,提出瞭解決方法,下面着手去實現
``` public class InitActivity extends FragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//這裏默認在SD卡根目錄,實際開發過程中可以把dex文件放在服務器,在啟動頁下載後加載進來
//第二次進入的時候可以根據目錄下是否已經下載過,處理,避免重新下載
//最後根據當前app版本下載不同的修復dex包 等等一系列處理
String dexFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/fix.dex";
DexFile dexFile = null;
try {
dexFile = DexFile.loadDex(dexFilePath, null, Context.MODE_PRIVATE);
} catch (IOException e) {
e.printStackTrace();
}
patchDex(dexFile);
startActivity(new Intent(this, MainActivity.class));
}
/**
* 修復過程,可以放在啟動頁,這樣在等待的過程中,網絡下載修復dex文件
*
* @param dexFile
*/
public void patchDex(DexFile dexFile) {
if (dexFile == null) return;
Enumeration<String> enumeration = dexFile.entries();
String className;
//遍歷dexFile中的類
while (enumeration.hasMoreElements()) {
className = enumeration.nextElement();
//加載修復後的類,只能修復當前Activity後加載類(可以放入Application中執行)
dexFile.loadClass(className, getClassLoader());
}
}
} ``` 方法很簡單在啟動頁,或者Application中提前加載有bug的類
這裏寫的很簡單,只是展示核心代碼,實際開發過程中,dex包下載的網絡請求,據當前app版本下載不同的修復dex,文件存在的時候可以在Application中先加載一次,啟動頁就不用加載,等等,一系列優化和判斷處理,這裏就不過多説明,具體一些處理看github上的代碼
ok 代碼都瞭解了,這個 fix.dex
文件哪裏來的呢
熟悉Android apk生成的小夥伴都知道了,跳過這個步驟,不懂的小夥伴繼續往下看
上面的InitActivity
中startActivity(new Intent(this, MainActivity.class));
啟動了一個MainActivity
看看我的MainActivity
```
public class MainActivity extends FragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//0不能做被除數,這裏會報ArithmeticException異常
Toast.makeText(this, "結果" + 10 / 0, Toast.LENGTH_LONG).show();
}
}
哎呀不小心,寫了一個bug 0 咋能做除數呢,app已經上線了,這裏必崩啊,咋辦
不要急,按照以下步驟:
1. 我們要修復這個類`MainActivity`,先把bug解決
Toast.makeText(this, "結果" + 10 / 2, Toast.LENGTH_LONG).show();
2. 把修復類生成`.class`文件(可以先run一次,之後在 build/intermediates/javac/debug/classes/com開的的文件夾,找到生成的class文件,也可以通過javac 命令行生成,也可以通過右邊的gradle Task生成)

3. 把修復類`.class`文件 打包成dex (其他`.class`刪除,只保留修復類) 打開cmd命令行,輸入下面命令
D:\Android\sdk\build-tools\28.0.3\dx.bat --dex --output C:\Users\pei\Desktop\dx\fix.dex C:\Users\pei\Desktop\dx\
``
D:\Android\sdk為自己sdk目錄
28.0.3為
build-tools版本,可以根據自己已經下載的版本更換
後面兩個目錄分別是生成
.dex文件目錄,和
.class`文件目錄
切記
.class
文件的目錄必須是包名一樣的,我的目錄是C:\Users\pei\Desktop\dx\com\pei\test\MainActivity.class
,不然會報class name does not match path
- 這樣dx文件夾下就會生成fix.dex文件了,把fix.dex放進手機根目錄試試吧
再次打開App,完美Toast 結果5,完美解決
總結
- 修復方法要在bug類之前執行
- 適合少量bug,太多bug影響性能
- 目前只能修復類,不能修復資源文件
- 目前只能適配單dex的項目,多dex的項目由於當前類和所有的引用類在同一個dex會 當前類被打上CLASS_ISPREVERIFIED標記,被打上這個標記的類不能引用其他dex中的類,否則就會報錯 解決辦法是在構造方法裏引用一個單獨的dex中的類,這樣不符合規則就不會被標記了