首先我們要知道Activity是如何啟動的,在文章http://www.lxweimin.com/p/bd5208574430中我們已經看了Activity啟動的源碼,http://www.lxweimin.com/p/40f436881390 ClassLoader加載類的源碼,這里再簡單回顧一下
看了Activity的啟動流程和ClassLoader加載類的方式之后,我們知道,當一個Activity要啟動的時候,最終其實是通過從BaseDexClassLoader中的一個DexPathList類中的dexElements數組中取出來,反射得到對象返回的
那么當開發中出現了問題,比如一個Activity中發生了bug,我們要緊急修復這個問題,同時又不需要應用重啟,該怎么做呢,熱修復的解決方案有幾個可選的阿里 AndFix(不再維護了,并且不支持8.0及以上,不建議使用),騰訊Tinker(需要重啟才能生效)。不過今天我們不使用第三方的實現,只從源碼方面找答案
關鍵點就在于dexElements這個數組,經過我們的分析,我們已經知道,所有的類的dexFile文件在應用啟動時就已經被保存的這個數組中了,包括發生了錯誤的文件類,當類被加載的時候,會從前往后遍歷這個數組,找到對應的class然后反射得到對象,那么我們的解決辦法是,能否將正確的文件類插入到這個數組中,當這個類啟動的時候,先加載到我們修復的正確的dexFile文件,從而達到修復的目的,要使用的技術就是反射
實現原理借鑒了騰訊的Tinker,不過Tinker核心思想是利用DexDiff算法對比差異生成Patch補丁包,將生成的差異dex文件插入dexElements,而我們做的卻少了生成差分包的過程,是將整個dex文件插入替換,Tinker原理圖如下:
我們當前要實現的思路就是現上有一個發生bug的app有待修復,我們在線下生成一個修復后的apk,將其后綴改為.zip,然后解壓打開,得到里邊的classes.dex文件,客戶端下載這個修復的dex到本地,然后將這個dex插入本地apk的dex數組中實現修復bug的功能。
構造方法執行,初始化dex文件的存儲位置
public FixDexManager(Context context) {
this.mContext = context;
//獲取系統能夠訪問的dex目錄
this.mDexDir = context.getDir("odex",Context.MODE_PRIVATE);
}
遍歷所有dex文件,存入集合中,這樣做也是仿照了AndFix的處理方式,目錄下可能存在多個dex文件,所以我們需要將他們都放入集合中,然后開始修復
public void loadFixDex() throws Exception{
File[] dexFiles = mDexDir.listFiles();
List<File> fixDexFiles = new ArrayList<>();
for (File dexFile : dexFiles) {
if (dexFile.getName().endsWith(".dex")){
fixDexFiles.add(dexFile);
}
}
fixDexFiles(fixDexFiles);
}
進入fixDexFiles方法
加載到程序已經運行的dexElements數組,這個數組包括存在問題的類,然后通過BaseDexClassLoader加載得到我們修復后的dex文件中的dexElements數組,最終將這個正確的dexElements數組插入到程序已經運行的dexElements中,從而當程序要啟動一個類的時候,會從數組中獲取到正確的類,達到修復bug的目的
private void fixDexFiles(List<File> fixDexFiles) throws Exception{
//1.先獲取已經運行的dexElement
ClassLoader applicationClassLoader = mContext.getClassLoader();
//Element數組對象
Object applicationDexElements = getDexElementByClassLoader(applicationClassLoader);
File optimizedDirectory = new File(mDexDir,"odex");
if (!optimizedDirectory.exists()){
optimizedDirectory.mkdirs();
}
//修復
for (File fixDexFile : fixDexFiles) {
//參數:
// String dexPath, dex路徑
// File optimizedDirectory,
// String librarySearchPath, so文件位置
// ClassLoader parent 父classloader
ClassLoader fixDexClassLoader = new BaseDexClassLoader(
fixDexFile.getAbsolutePath(),//dex路徑,必須要在應用目錄下的odex文件中
optimizedDirectory,
null,
applicationClassLoader
);
Object fixDexElements = getDexElementByClassLoader(fixDexClassLoader);
//3.將下載的dex插入到已經運行的dexElement的最前邊,合并
//applicationClassLoader 數組合并fixDexElements數組
applicationDexElements = combineArray(fixDexElements, applicationDexElements);
//把合并的數組注入到原來的類中 applicationClassLoader
injectDexElements(applicationClassLoader, applicationDexElements);
}
}
通過反射再次獲取到dexElements這個數組的Field,和它所在DexPathList的對象,注入即可
//把dexElements注入到classLoader中
private void injectDexElements(ClassLoader classLoader, Object dexElements) throws Exception{
//先獲取pathList
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader);
//獲取pathList中的dexElements
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
//利用反射進行注入
dexElementsField.set(pathList,dexElements);
}
使用方法:
在Application啟動的時候初始化并調用修復方法,這種實現方式有很大的問題,因為是將修復后的dex插入原來的dex數組,以保證加載類的時候可以加載到正確的類,那么這種情況下,如果當前bug頁面已經加載出來,這時候再通過這種方式注入dex是不會起作用的,必須在啟動bug類之前將dex注入,否則如果不重啟,不會有效果。所以這種方式實現的熱修復必須要重啟生效。
FixDexManager manager = new FixDexManager(this);
//加載所有修復的dex包,第一次的時候會從服務器上下載到修復的dex包并放在我們制定的目錄下,
//第二次進入的時候,直接讀取保存的文件進行修復
try {
manager.loadFixDex();
File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(),"fix.apatch");
if (file.exists()){
try {
manager.fixDex(file.getAbsolutePath());
Toast.makeText(getApplicationContext(),"修復bug成功",Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(getApplicationContext(),"修復bug失敗",Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
總結一下:
通過這種方式實現熱修復,需要重啟app,它的缺點也很明顯,由于是將整個dex文件注入到原來dex數組中,會使app內存占用增大一倍左右,我這邊親測,原本占空間10.8M的app,注入新的dex后,內存占用變成了19.8M,可以考慮分包的方案解決這個問題,減少體積。