熱修復
目前國內Android熱修復技術已經發展的可以說百花齊放了,從實現方式來大致分類,可以分爲:
- Native層實現
- Java層實現
之前也有人簡單分析過阿里開源的Andfix
實現原理(基於Native層),這裏就不再多說了感興趣的可以搜索看下。
本文簡單分析一下Java層實現熱修復邏輯,簡單實現代碼熱修復Demo,以Tinker爲例(當然Tinker
是支持代碼修復,資源修復,so修復
的,感興趣的小夥伴自行移步官網~)
先梳理下思路:
Java類編譯過程
就是java類通過javac編譯成.class文件,再由dx.bat編譯成.dex文件的過程,不做贅述,簡單畫張圖~
ClassLoader簡介
Android中的java.lang.ClassLoader這個類也不同於Java中的java.lang.ClassLoader。 Android中的ClassLoader類型也可分爲系統ClassLoader和自定義ClassLoader。其中系統ClassLoader包括3種分別是:
BootClassLoader
,Android系統啓動時會使用BootClassLoader來預加載常用類,與Java中的Bootstrap ClassLoader不同的是,它並不是由C/C++代碼實現,而是由Java實現的。BootClassLoader是ClassLoader的一個內部類。PathClassLoader
,全名是dalvik/system.PathClassLoader,可以加載已經安裝的Apk,也就是/data/app/package 下的apk文件,也可以加載/vendor/lib, /system/lib下的nativeLibrary。DexClassLoader
,全名是dalvik/system.DexClassLoader,可以加載一個未安裝的apk文件。 PathClassLoader和DexClasLoader都是繼承自 dalviksystem.BaseDexClassLoader,它們的類加載邏輯全部寫在BaseDexClassLoader中。 下圖展示了Android中的ClassLoader中的繼承體系,其中SecureClassLoader和UrlClassLoader是在Java中的類加載器,在Android中是沒法辦使用的。
.dex文件加載
通過源碼得知,.dex
文件是通過BaseDexClassLoader類(ClassLoader的子類)
進行加載的,這個類裏面有個成員變量DexPathList對象
,而這個對象中有個有個數組存放的是DexElement
對象,也就是從文件中加載的.dex
文件,切入點就在這裏
對於項目來說,一般項目會分包(方法數大於64k,及大於65535個時候,google提供的分包策略),如果採用Java代碼實現熱修復,分包是肯定要做的,因爲要保證主包沒有bug,分包簡單說就是打包的apk一般有多個.dex
文件
如: classes.dex,classes2.dex 等等…
那麼比如我們的classes2.dex
中某個類的某個方法出現了異常,我們就可以創建一個修復包(修復後的classes2.dex
文件),然後通過自定義的類加載器將修復後的classes2.dex
文件copy到私有目錄,再插隊到系統ClassLoader
的dexPathList對象
的dexElement
的數組中,讓系統優先加載修復後的classes2.dex
文件,以做到熱修復的目的,這種實現方式必須要執行修復邏輯後,重啓app才能實現效果~
瞭解了這些信息大致思路就有了,我們需要修復後的.dex文件加載解析,然後插隊舊的安裝包裝的.dex文件,做到插隊的操作,相當於欺騙了Android系統,大致如下:
實現原理
思路大概是,我們需要一個修復bug的.dex文件,插隊到BaseDexClassLoader類
下的DexPathList對象
的DexElement
數組中,並且排序到最前面,讓系統加載到我們修復後的.dex文件不會再加載有bug的dex文件,完成插隊(插裝),這裏會有個類加載機制的知識,本文不做詳細介紹,後面會單獨寫一篇總結~ 大致實現步驟如下:
Demo實現
1、基礎配置-主包配置
配置分包,配置分包的目的主要是打包做出來的apk會有多個.dex文件,實際項目應用中要保證主包不要有bug,demo中加載.dex文件的時候也排除了主包文件classes.dex
,大致如下: 創建BaseApplication
,BaseActivity
,MainActivity
放置在主包,其中MainActivity
主要是爲了分包占位,只做了點擊跳轉分包中SecondActivity
的邏輯 app目錄下的build.gradle
開啓分包支持,在android
→defaultConfig
下增加配置,其中multiDex-config.txt
是配置保留在主包內類文件
//開啓分包
multiDexEnabled true
//分包的配置,將配置文件中的放置在主包
multiDexKeepFile file("multiDex-config.txt")
添加分包依賴:
//multidex分包依賴
implementation 'com.android.support:multidex:1.0.3'
Application開啓分包:
public class BaseApplication extends MultiDexApplication {
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//安裝分包配置
MultiDex.install(this);
}
}
2、分包配置
分包就創建了一個SecondActivity類
做模擬異常和修復異常的入口,和一個Calculate
模擬異常,做了10/0
的操作,修復後爲10/1
注:獲取修復後的
classes2.dex
文件可以通過直接buildapk直接解壓獲取,或者用build-tools下的dx.bat
執行命令獲取
簡單貼下SecondActivity
,點擊fix
按鈕後的代碼:
private void update() {
//將下載的修復包,複製到私有目錄,解壓從.dex文件中取到對應的.class文件
//從sd卡取修復包
File sourceFile = new File(Environment.getExternalStorageDirectory(), Constants.DEX_NAME);
//目標文件
File targetFile = new File(getDir(Constants.DEX_DIR, Context.MODE_PRIVATE).getAbsolutePath() + File.separator + Constants.DEX_NAME);
if (targetFile.exists()) {
targetFile.delete();
Log.e("update","刪除原有dex文件(已使用的)");
}
//將SD卡中的修復包copy到私有目錄
FileUtils.copyFile(sourceFile,targetFile);
Log.e("update","copy完成");
FixDexUtils.loadDexFile(this);
}
3、FixModule
新建一個Module處理熱修復的相關邏輯
只有五個文件,核心文件代碼在FixDexUtils
,其它是工具類,還有個定義了幾個常量 看下FixDexUtils
中的代碼
public class FixDexUtils {
//修復文件可能有多個
private static HashSet<File> loadedDex = new HashSet<>();
//不建議這麼寫,demo演示用
static {
loadedDex.clear();
}
public static void loadDexFile(Context context) {
//獲取私有目錄
File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
//遍歷篩選私有目錄中的.dex文件
File[] listFiles = fileDir.listFiles();
for (int i = 0; i < listFiles.length; i++) {
//文件名以.dex結尾,且不是主包.dex文件
if (listFiles[i].getName().endsWith(Constants.DEX_SUFFIX) && !"classes.dex".equalsIgnoreCase(listFiles[i].getName())) {
loadedDex.add(listFiles[i]);
}
}
//創建自定義的類加載器
createDexClassLoader(context ,fileDir);
}
/**
* @param context
* @param fileDir
* 創建自己的類加載器,加載私有目錄的.dex文件,上面已經將修復包中的dex文件copy到私有目錄
*/
private static void createDexClassLoader(Context context, File fileDir) {
//解壓目錄
String optimizedDir = fileDir.getAbsolutePath()+File.separator+"opt_dex";
File fileOpt = new File(optimizedDir);
if (!fileOpt.exists()) {
fileOpt.mkdirs();
}
for (File dex : loadedDex) {
//創建自己的類加載器,臨時的
DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(), optimizedDir, null, context.getClassLoader());
//有一個修復文件,就插裝一次
hotFix(classLoader,context);
}
}
private static void hotFix(DexClassLoader classLoader, Context context) {
try {
//獲取系統的PathClassLoader類加載器
PathClassLoader pathClassLoader = (PathClassLoader)context.getClassLoader();
//獲取自己的dexElements數組
Object myElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(classLoader));
//獲取系統的dexElements數組
Object systemElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(pathClassLoader));
//合併數組,並排序,生成一個新的數組
Object dexElements=ArrayUtils.combineArray(myElements,systemElements);
//通過反射獲取系統的pathList屬性
Object systemPathList = ReflectUtils.getPathList(pathClassLoader);
//通過反射,將合併後新的dexElements賦值給系統的pathList
ReflectUtils.setFieldValue(systemPathList,"dexElements",dexElements);
}catch (Exception e){
e.printStackTrace();
}
}
}
主要做的工作就是:就是上面那張流程圖,就是先通過遍歷,解壓等操作,獲取到需要執行熱修復的.dex
文件集合,遍歷該集合,對應每次創建一個臨時的DexClassLoader
,然後執行修復步驟,細分就是六步:
最終實現效果如圖(Demo使用手機是華爲8.0的手機):
注:這裏爲了效果圖更直觀,已經重啓過一次app
注: 這種方式實現的熱修復必須要重啓App纔可以實現修復,這一點也是類加載機制決定的,如下圖中修復之後再次打開加載執行修復後的classes.dex
文件是在BaseApplication
中調用了修復方法
最後
在這裏我也分享一份由幾位大佬一起收錄整理的Android學習PDF+架構視頻+面試文檔+源碼筆記,高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料
如果你有需要的話,可以 點這領取
喜歡本文的話,不妨順手給我點個小贊、評論區留言或者轉發支持一下唄~