現(xiàn)有的熱修復(fù)框架很多,尤以AndFix 和Tinker比較多
具體的實(shí)現(xiàn)方式和項(xiàng)目引用可以參考網(wǎng)絡(luò)上的文章,今天就不談,也不是主要目的
今天就來(lái)探討,如何手寫(xiě)一個(gè)熱修復(fù)的功能
對(duì)于簡(jiǎn)單的項(xiàng)目,不想集成其他修復(fù)框架的SDK,也不想用第三方平臺(tái),只是緊急修復(fù)一些bug
還是挺方便的
言歸正傳,如果一個(gè)或多個(gè)類(lèi)出現(xiàn)bug,導(dǎo)致了崩潰或者數(shù)據(jù)顯示異常,如果修復(fù)呢,如果熟悉jvm dalvik 類(lèi)的加載機(jī)制,就會(huì)清楚的了解 ClassLoader的 雙親委托機(jī)制 就可以通過(guò)這個(gè)
什么是雙親委托機(jī)制
- 當(dāng)前ClassLoader首先從自己已經(jīng)加載的類(lèi)中查詢(xún)是否此類(lèi)已經(jīng)加載,如果已經(jīng)加載則直接返回原來(lái)已經(jīng)加載的類(lèi)。
每個(gè)類(lèi)加載器都有自己的加載緩存,當(dāng)一個(gè)類(lèi)被加載了以后就會(huì)放入緩存,等下次加載的時(shí)候就可以直接返回了。 - 當(dāng)前classLoader的緩存中沒(méi)有找到被加載的類(lèi)的時(shí)候,委托父類(lèi)加載器去加載,父類(lèi)加載器采用同樣的策略,首先查看自己的緩存,然后委托父類(lèi)的父類(lèi)去加載,一直到bootstrp ClassLoader.
- 當(dāng)所有的父類(lèi)加載器都沒(méi)有加載的時(shí)候,再由當(dāng)前的類(lèi)加載器加載,并將其放入它自己的緩存中,以便下次有加載請(qǐng)求的時(shí)候直接返回。
突破口來(lái)了,看1(如果已經(jīng)加載則直接返回原來(lái)已經(jīng)加載的類(lèi))
對(duì)于同一個(gè)類(lèi),如果先加載修復(fù)的類(lèi),當(dāng)后續(xù)在加載未修復(fù)的類(lèi)的時(shí)候,直接返回修復(fù)的類(lèi),這樣bug不就解決了嗎?
Nice ,多看源碼和jvm 許多問(wèn)題可以從framework和底層去解決
話(huà)不多說(shuō),提出了解決方法,下面著手去實(shí)現(xiàn)
public class InitActivity extends FragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//這里默認(rèn)在SD卡根目錄,實(shí)際開(kāi)發(fā)過(guò)程中可以把dex文件放在服務(wù)器,在啟動(dòng)頁(yè)下載后加載進(jìn)來(lái)
//第二次進(jìn)入的時(shí)候可以根據(jù)目錄下是否已經(jīng)下載過(guò),處理,避免重新下載
//最后根據(jù)當(dāng)前app版本下載不同的修復(fù)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));
}
/**
* 修復(fù)過(guò)程,可以放在啟動(dòng)頁(yè),這樣在等待的過(guò)程中,網(wǎng)絡(luò)下載修復(fù)dex文件
*
* @param dexFile
*/
public void patchDex(DexFile dexFile) {
if (dexFile == null) return;
Enumeration<String> enumeration = dexFile.entries();
String className;
//遍歷dexFile中的類(lèi)
while (enumeration.hasMoreElements()) {
className = enumeration.nextElement();
//加載修復(fù)后的類(lèi),只能修復(fù)當(dāng)前Activity后加載類(lèi)(可以放入Application中執(zhí)行)
dexFile.loadClass(className, getClassLoader());
}
}
}
方法很簡(jiǎn)單在啟動(dòng)頁(yè),或者Application中提前加載有bug的類(lèi)
這里寫(xiě)的很簡(jiǎn)單,只是展示核心代碼,實(shí)際開(kāi)發(fā)過(guò)程中,dex包下載的網(wǎng)絡(luò)請(qǐng)求,據(jù)當(dāng)前app版本下載不同的修復(fù)dex,文件存在的時(shí)候可以在A(yíng)pplication中先加載一次,啟動(dòng)頁(yè)就不用加載,等等,一系列優(yōu)化和判斷處理,這里就不過(guò)多說(shuō)明,具體一些處理看github上的代碼
ok 代碼都了解了,這個(gè) fix.dex 文件哪里來(lái)的呢
熟悉Android apk生成的小伙伴都知道了,跳過(guò)這個(gè)步驟,不懂的小伙伴繼續(xù)往下看
上面的InitActivity中startActivity(new Intent(this, MainActivity.class)); 啟動(dòng)了一個(gè)MainActivity
看看我的MainActivity
public class MainActivity extends FragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//0不能做被除數(shù),這里會(huì)報(bào)ArithmeticException異常
Toast.makeText(this, "結(jié)果" + 10 / 0, Toast.LENGTH_LONG).show();
}
}
哎呀不小心,寫(xiě)了一個(gè)bug 0 咋能做除數(shù)呢,app已經(jīng)上線(xiàn)了,這里必崩啊,咋辦
不要急,按照以下步驟:
- 我們要修復(fù)這個(gè)類(lèi)
MainActivity,先把bug解決
Toast.makeText(this, "結(jié)果" + 10 / 2, Toast.LENGTH_LONG).show();
- 把修復(fù)類(lèi)生成
.class文件(可以先run一次,之后在 build/intermediates/javac/debug/classes/com開(kāi)的的文件夾,找到生成的class文件,也可以通過(guò)javac 命令行生成,也可以通過(guò)右邊的gradle Task生成)
class 路徑圖 - 把修復(fù)類(lèi)
.class文件 打包成dex (其他.class刪除,只保留修復(fù)類(lèi)) 打開(kāi)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版本,可以根據(jù)自己已經(jīng)下載的版本更換
后面兩個(gè)目錄分別是生成.dex文件目錄,和.class文件目錄
切記
.class文件的目錄必須是包名一樣的,我的目錄是C:\Users\pei\Desktop\dx\com\pei\test\MainActivity.class,不然會(huì)報(bào)class name does not match path
- 這樣dx文件夾下就會(huì)生成fix.dex文件了,把fix.dex放進(jìn)手機(jī)根目錄試試吧
再次打開(kāi)App,完美Toast 結(jié)果5,完美解決
總結(jié)
- 修復(fù)方法要在bug類(lèi)之前執(zhí)行
- 適合少量bug,太多bug影響性能
- 目前只能修復(fù)類(lèi),不能修復(fù)資源文件
- 目前只能適配單dex的項(xiàng)目,多dex的項(xiàng)目由于當(dāng)前類(lèi)和所有的引用類(lèi)在同一個(gè)dex會(huì) 當(dāng)前類(lèi)被打上CLASS_ISPREVERIFIED標(biāo)記,被打上這個(gè)標(biāo)記的類(lèi)不能引用其他dex中的類(lèi),否則就會(huì)報(bào)錯(cuò)
解決辦法是在構(gòu)造方法里引用一個(gè)單獨(dú)的dex中的類(lèi),這樣不符合規(guī)則就不會(huì)被標(biāo)記了
