熱修復(fù)
目前國內(nèi)Android熱修復(fù)技術(shù)已經(jīng)發(fā)展的可以說百花齊放了,從實(shí)現(xiàn)方式來大致分類,可以分為:
① Native層實(shí)現(xiàn)
② Java層實(shí)現(xiàn)
之前有簡單分析過阿里開源的Andfix
實(shí)現(xiàn)原理(基于Native層),詳見Android_熱修復(fù)_Andfix原理分析,這里就多說了,感興趣的可以看下
本文簡單分析一下Java層實(shí)現(xiàn)熱修復(fù)邏輯,簡單實(shí)現(xiàn)代碼熱修復(fù)Demo,以Tinker為例(當(dāng)然Tinker
是支持代碼修復(fù),資源修復(fù),so修復(fù)
的,感興趣的小伙伴自行移步官網(wǎng)~)
先梳理下思路:
Java類編譯過程
就是java類通過javac編譯成.class文件,再由dx.bat編譯成.dex文件的過程,不做贅述,簡單畫張圖~
ClassLoader簡介
Android中的java.lang.ClassLoader這個(gè)類也不同于Java中的java.lang.ClassLoader狰闪。
Android中的ClassLoader類型也可分為系統(tǒng)ClassLoader和自定義ClassLoader挤渔。其中系統(tǒng)ClassLoader包括3種分別是:
-
BootClassLoader
挎春,Android系統(tǒng)啟動時(shí)會使用BootClassLoader來預(yù)加載常用類参滴,與Java中的Bootstrap ClassLoader不同的是泡一,它并不是由C/C++代碼實(shí)現(xiàn)加缘,而是由Java實(shí)現(xiàn)的。BootClassLoader是ClassLoader的一個(gè)內(nèi)部類韧骗。 -
PathClassLoader
翎苫,全名是dalvik/system.PathClassLoader权埠,可以加載已經(jīng)安裝的Apk,也就是/data/app/package 下的apk文件煎谍,也可以加載/vendor/lib, /system/lib下的nativeLibrary攘蔽。 -
DexClassLoader
,全名是dalvik/system.DexClassLoader呐粘,可以加載一個(gè)未安裝的apk文件满俗。
PathClassLoader和DexClasLoader都是繼承自 dalviksystem.BaseDexClassLoader,它們的類加載邏輯全部寫在BaseDexClassLoader中作岖。
下圖展示了Android中的ClassLoader中的繼承體系唆垃,其中SecureClassLoader和UrlClassLoader是在Java中的類加載器,在Android中是沒法辦使用的鳍咱。
.dex文件加載
通過源碼得知,.dex
文件是通過BaseDexClassLoader類(ClassLoader的子類)
進(jìn)行加載的,這個(gè)類里面有個(gè)成員變量DexPathList對象
,而這個(gè)對象中有個(gè)有個(gè)數(shù)組存放的是DexElement
對象,也就是從文件中加載的.dex
文件,切入點(diǎn)就在這里,對于項(xiàng)目來說,一般項(xiàng)目會分包(方法數(shù)大于64k,及大于65535個(gè)時(shí)候,google提供的分包策略),如果采用Java代碼實(shí)現(xiàn)熱修復(fù),分包是肯定要做的,因?yàn)橐WC主包沒有bug,分包簡單說就是打包的apk一般有多個(gè).dex
文件,如 classes.dex,classes2.dex 等等...
那么比如我們的classes2.dex
中某個(gè)類的某個(gè)方法出現(xiàn)了異常,我們就可以創(chuàng)建一個(gè)修復(fù)包(修復(fù)后的classes2.dex
文件),然后通過自定義的類加載器將修復(fù)后的classes2.dex
文件copy
到私有目錄,再插隊(duì)到系統(tǒng)ClassLoader
的dexPathList對象
的dexElement
的數(shù)組中,讓系統(tǒng)優(yōu)先加載修復(fù)后的classes2.dex
文件,以做到熱修復(fù)的目的,這種實(shí)現(xiàn)方式必須要執(zhí)行修復(fù)邏輯后,重啟app才能實(shí)現(xiàn)效果~
了解了這些信息大致思路就有了,我們需要修復(fù)后的.dex文件加載解析,然后插隊(duì)舊的安裝包裝的.dex文件,做到插隊(duì)的操作,相當(dāng)于欺騙了Android系統(tǒng),大致如下:
實(shí)現(xiàn)原理
思路大概是,我們需要一個(gè)修復(fù)bug的.dex文件,插隊(duì)到BaseDexClassLoader類
下的DexPathList對象
的DexElement
數(shù)組中,并且排序到最前面,讓系統(tǒng)加載到我們修復(fù)后的.dex文件不會再加載有bug的dex文件,完成插隊(duì)(插裝),這里會有個(gè)類加載機(jī)制的知識,本文不做詳細(xì)介紹,后面會單獨(dú)寫一篇總結(jié)~
大致實(shí)現(xiàn)步驟如下:
Demo實(shí)現(xiàn)
① 基礎(chǔ)配置-主包配置
配置分包,配置分包的目的主要是打包做出來的apk會有多個(gè).dex文件,實(shí)際項(xiàng)目應(yīng)用中要保證主包不要有bug,demo中加載.dex文件的時(shí)候也排除了主包文件classes.dex
,大致如下:
創(chuàng)建BaseApplication
,BaseActivity
,MainActivity
放置在主包,其中MainActivity
主要是為了分包占位,只做了點(diǎn)擊跳轉(zhuǎn)分包中SecondActivity
的邏輯
app目錄下的build.gradle
開啟分包支持,在android
→defaultConfig
下增加配置,其中multiDex-config.txt
是配置保留在主包內(nèi)類文件
//開啟分包
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);
}
}
② 分包配置
分包就創(chuàng)建了一個(gè)SecondActivity類
做模擬異常和修復(fù)異常的入口,和一個(gè)Calculate
模擬異常,做了10/0
的操作,修復(fù)后為10/1
注:
獲取修復(fù)后的classes2.dex
文件可以通過直接buildapk直接解壓獲取,或者用build-tools下的dx.bat
執(zhí)行命令獲取,這篇文章中提及: Android_熱修復(fù)_Andfix原理分析
簡單貼下SecondActivity
,點(diǎn)擊fix
按鈕后的代碼,完整代碼見文末~:
private void update() {
//將下載的修復(fù)包,復(fù)制到私有目錄,解壓從.dex文件中取到對應(yīng)的.class文件
//從sd卡取修復(fù)包
File sourceFile = new File(Environment.getExternalStorageDirectory(), Constants.DEX_NAME);
//目標(biāo)文件
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卡中的修復(fù)包c(diǎn)opy到私有目錄
FileUtils.copyFile(sourceFile,targetFile);
Log.e("update","copy完成");
FixDexUtils.loadDexFile(this);
}
③FixModule
新建一個(gè)Module處理熱修復(fù)的相關(guān)邏輯
只有五個(gè)文件,核心文件代碼在FixDexUtils
,其它是工具類,還有個(gè)定義了幾個(gè)常量
看下FixDexUtils
中的代碼
public class FixDexUtils {
//修復(fù)文件可能有多個(gè)
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結(jié)尾,且不是主包.dex文件
if (listFiles[i].getName().endsWith(Constants.DEX_SUFFIX) && !"classes.dex".equalsIgnoreCase(listFiles[i].getName())) {
loadedDex.add(listFiles[i]);
}
}
//創(chuàng)建自定義的類加載器
createDexClassLoader(context ,fileDir);
}
/**
* @param context
* @param fileDir
* 創(chuàng)建自己的類加載器,加載私有目錄的.dex文件,上面已經(jīng)將修復(fù)包中的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) {
//創(chuàng)建自己的類加載器,臨時(shí)的
DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(), optimizedDir, null, context.getClassLoader());
//有一個(gè)修復(fù)文件,就插裝一次
hotFix(classLoader,context);
}
}
private static void hotFix(DexClassLoader classLoader, Context context) {
try {
//獲取系統(tǒng)的PathClassLoader類加載器
PathClassLoader pathClassLoader = (PathClassLoader)context.getClassLoader();
//獲取自己的dexElements數(shù)組
Object myElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(classLoader));
//獲取系統(tǒng)的dexElements數(shù)組
Object systemElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(pathClassLoader));
//合并數(shù)組,并排序,生成一個(gè)新的數(shù)組
Object dexElements=ArrayUtils.combineArray(myElements,systemElements);
//通過反射獲取系統(tǒng)的pathList屬性
Object systemPathList = ReflectUtils.getPathList(pathClassLoader);
//通過反射,將合并后新的dexElements賦值給系統(tǒng)的pathList
ReflectUtils.setFieldValue(systemPathList,"dexElements",dexElements);
}catch (Exception e){
e.printStackTrace();
}
}
}
主要做的工作就是:就是上面那張流程圖,就是先通過遍歷,解壓等操作,獲取到需要執(zhí)行熱修復(fù)的.dex
文件集合,遍歷該集合,對應(yīng)每次創(chuàng)建一個(gè)臨時(shí)的DexClassLoader
,然后執(zhí)行修復(fù)步驟,細(xì)分就是六步:
最終實(shí)現(xiàn)效果如圖(Demo使用手機(jī)是華為8.0的手機(jī)):
注:這里為了效果圖更直觀,已經(jīng)重啟過一次app
注:
這種方式實(shí)現(xiàn)的熱修復(fù)必須要重啟App才可以實(shí)現(xiàn)修復(fù),這一點(diǎn)也是類加載機(jī)制決定的,如下圖中修復(fù)之后再次打開加載執(zhí)行修復(fù)后的classes.dex
文件是在BaseApplication
中調(diào)用了修復(fù)方法,詳見完整代碼