前言
自己之前也做過插件化換膚,涉及到的是插件資源文件的加載;最近看到同事培訓(xùn)的插件化涉及到具體代碼的加載;想自己了解一下断楷,就先從最常用的熱修復(fù)開始看起,由于剛開始接觸相關(guān)的概念崭别,理解也不是很深冬筒,但是總體看下來還是比較簡單的,這里記錄一下自己的理解茅主;
熱修復(fù)的應(yīng)用場景
熱修復(fù)就是在APP上線以后舞痰,如果突然發(fā)現(xiàn)有缺陷了,如果重新走發(fā)布流程可能時間比較長诀姚,重新安裝APP用戶體驗也不會太好响牛;熱修復(fù)就是通過發(fā)布一個插件,使APP運(yùn)行的時候加載插件里面的代碼,從而解決缺陷呀打,并且對于用戶來說是無感的(用戶也可能需要重啟一下APP)矢赁。
熱修復(fù)的原理
先說結(jié)論吧,就是將補(bǔ)丁 dex 文件放到 dexElements 數(shù)組靠前位置贬丛,這樣在加載 class 時撩银,優(yōu)先找到補(bǔ)丁包中的 dex 文件,加載到 class 之后就不再尋找瘫寝,從而原來的 apk 文件中同名的類就不會再使用蜒蕾,從而達(dá)到修復(fù)的目的
理解這個原理稠炬,需要了解一下Android的代碼加載的機(jī)制焕阿;
Android運(yùn)行流程
簡單來講整體流程是這樣的:
1、Android程序編譯的時候首启,會將.java文件編譯時.class文件
2暮屡、然后將.class文件打包為.dex文件
3、然后Android程序運(yùn)行的時候毅桃,Android的Dalvik/ART虛擬機(jī)就加載.dex文件
4褒纲、加載其中的.class文件到內(nèi)存中來使用
類加載器
負(fù)責(zé)加載這些.class文件的就是類加載器(ClassLoader),APP啟動的時候钥飞,會創(chuàng)建一個自己的ClassLoader實例莺掠,我們可以通過下面的代碼拿到當(dāng)前的ClassLoader
ClassLoader classLoader = getClassLoader();
Log.i(TAG, "[onCreate] classLoader" + ":" + classLoader.toString());
ClassLoader加載類的方法就是loadClass可以看一下源碼,是通過雙親委派模型(Parents Delegation Model)读宙,它首先不會自己去嘗試加載這個類彻秆, 而是把這個請求委派給父類加載器去完成,當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類) 時结闸, 子加載器才會嘗試自己去完成加載唇兑,最后是調(diào)用自己的findClass方法完成的
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
ClassLoader是一個抽象類,通過打印可以看出來當(dāng)前的ClassLoader是一個PathClassLoader桦锄;看一下PathClassLoader的構(gòu)造函數(shù)扎附,可以看出,需要傳入一個dexPath也就是dex包的路徑结耀,和父類加載器留夜;
//dexPath 包含 dex 的 jar 文件或 apk 文件的路徑集,多個以文件分隔符分隔图甜,默認(rèn)是“:”
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
PathClassLoader是BaseDexClassLoader的子類香伴,除此之外BaseDexClassLoader還有一個子類是DexClassLoader,optimizedDirectory用來緩存優(yōu)化的 dex 文件的路徑具则,即從 apk 或 jar 文件中提取出來的 dex 文件即纲;
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
這兩個的區(qū)別,網(wǎng)上的答案是
1博肋、DexClassLoader可以加載jar/apk/dex低斋,可以從SD卡中加載未安裝的apk
2蜂厅、PathClassLoader只能加載系統(tǒng)中已經(jīng)安裝過的apk
從這個答案可以知道,我們想要加載更新的插件膊畴,肯定是使用 DexClassLoader掘猿;但是有點離譜的是其實我用兩個都能成功,也許我加載的插件包名這些都和原APP一致導(dǎo)致的吧唇跨。
類加載器的運(yùn)行流程
具體的實現(xiàn)都在BaseDexClassLoader里面稠通,看一下里面的實現(xiàn)(源碼看不了,網(wǎng)上搜一下)买猖,下面是一個構(gòu)造方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
構(gòu)造方法創(chuàng)建了一個DexPathLis改橘,里面解析了dex文件的路徑,并將解析的dex文件都存在this.dexElements里面
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
…
//將解析的dex文件都存在this.dexElements里面
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
}
//解析dex文件
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) || name.endsWith(ZIP_SUFFIX)) {
zip = new ZipFile(file);
}
……
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
} return elements.toArray(new Element[elements.size()]);
}
然后我們再回頭看一下ClassLoader加載類的方法,就是loadClass()玉控,最后調(diào)用findClass方法完成的;BaseDexClassLoader 重寫了該方法飞主,如下
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 使用pathList對象查找name類
Class c = pathList.findClass(name, suppressedExceptions);
return c;
}
最終是調(diào)用 pathList的findClass方法,看一下方法如下
public Class findClass(String name, List<Throwable> suppressed) {
// 遍歷從dexPath查詢到的dex和資源Element
for (Element element : dexElements) {
DexFile dex = element.dexFile;
// 如果當(dāng)前的Element是dex文件元素
if (dex != null) {
// 使用DexFile.loadClassBinaryName加載類
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
結(jié)論
所以整個類加載流程就是
1高诺、類加載器BaseDexClassLoader先將dex文件解析放到pathList到dexElements里面
2碌识、加載類的時候從dexElements里面去遍歷,看哪個dex里面有這個類就去加載虱而,生成class對象
所以我們可以將自己的dex文件加載到dexElements里面筏餐,并且放在前面,加載的時候就可以加載我們插件中的類牡拇,不會加載后面的,從而替換掉原來的class魁瞪。
熱修復(fù)的實現(xiàn)
知道了原理,實現(xiàn)就比較簡單了诅迷,就添加新的dex對象到當(dāng)前APP的ClassLoader對象(也就是BaseDexClassLoader)的pathList里面的dexElements佩番;要添加就要先創(chuàng)建,我們使用DexClassLoader先加載插件罢杉,先生成插件的dexElements趟畏,然后再添加就好了。
當(dāng)然整個過程需要使用反射來實現(xiàn)滩租。除此以外赋秀,常用的兩種方法是使用apk作為插件和使用dex文件作為插件;下面的兩個實現(xiàn)都是對程序中的一個方法進(jìn)行了修改律想,然后分別打了 dex包和apk包猎莲,程序運(yùn)行起來執(zhí)行的方法就是插件里面的方法而不是程序本身的方法;
dex插件
對于dex文件作為插件技即,和之前說的流程完全一致著洼,先將修改了的類進(jìn)行打包成dex包,將dex進(jìn)行加載,插入到dexElements集合的前面即可身笤;打包流程是先將.java文件編譯成.class文件豹悬,然后使用SDK工具打包成dex文件人,然后APP下載液荸,加載即可瞻佛;
dex打包工具
d8 作為獨(dú)立工具納入了 Android 構(gòu)建工具 28.0.1 及更高版本中:C:\Users\hanpei\AppData\Local\Android\Sdk\build-tools\29.0.2\d8.bat
;輸入字節(jié)碼可以是 *.class 文件或容器(例如 JAR娇钱、APK 或 ZIP 文件)的任意組合伤柄。您還可以添加 DEX 文件作為 d8 的輸入,以將這些文件合并到 DEX 輸出中
d8 MyProject/app/build/intermediates/classes/debug/*/*.class
具體的代碼實現(xiàn)
代碼的注釋已經(jīng)很詳細(xì)了文搂,就不再進(jìn)行說明了
//在Application中進(jìn)行替換
public class MApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//dex作為插件進(jìn)行加載
dexPlugin();
}
...
/**
* dex作為插件加載
*/
private void dexPlugin(){
//插件包文件
File file = new File("/sdcard/FixTest.dex");
if (!file.exists()) {
Log.i("MApplication", "插件包不在");
return;
}
try {
//獲取到 BaseDexClassLoader 的 pathList字段
// private final DexPathList pathList;
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
//破壞封裝适刀,設(shè)置為可以調(diào)用
pathListField.setAccessible(true);
//拿到當(dāng)前ClassLoader的pathList對象
Object pathListObj = pathListField.get(getClassLoader());
//獲取當(dāng)前ClassLoader的pathList對象的字節(jié)碼文件(DexPathList )
Class<?> dexPathListClass = pathListObj.getClass();
//拿到DexPathList 的 dexElements字段
// private final Element[] dexElements;
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
//破壞封裝细疚,設(shè)置為可以調(diào)用
dexElementsField.setAccessible(true);
//使用插件創(chuàng)建 ClassLoader
DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
//拿到插件的DexClassLoader 的 pathList對象
Object newPathListObj = pathListField.get(pathClassLoader);
//拿到插件的pathList對象的 dexElements變量
Object newDexElementsObj = dexElementsField.get(newPathListObj);
//拿到當(dāng)前的pathList對象的 dexElements變量
Object dexElementsObj=dexElementsField.get(pathListObj);
int oldLength = Array.getLength(dexElementsObj);
int newLength = Array.getLength(newDexElementsObj);
//創(chuàng)建一個dexElements對象
Object concatDexElementsObject = Array.newInstance(dexElementsObj.getClass().getComponentType(), oldLength + newLength);
//先添加新的dex添加到dexElement
for (int i = 0; i < newLength; i++) {
Array.set(concatDexElementsObject, i, Array.get(newDexElementsObj, i));
}
//再添加之前的dex添加到dexElement
for (int i = 0; i < oldLength; i++) {
Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObj, i));
}
//將組建出來的對象設(shè)置給 當(dāng)前ClassLoader的pathList對象
dexElementsField.set(pathListObj, concatDexElementsObject);
} catch (Exception e) {
e.printStackTrace();
}
}
apk插件
apk作為插件蔗彤,就是我們重新打了一個新的apk包作為插件川梅,打包很簡單方便疯兼,缺點就是文件大;使用apk的話就沒必要是將dex插入dexElements里面去贫途,直接將之前的dexElements替換就可以了吧彪;
具體的實現(xiàn)
代碼的注釋已經(jīng)很詳細(xì)了,就不再進(jìn)行說明了
/**
* apk作為插件加載
*/
private void apkPlugin() {
//插件包文件
File file = new File("/sdcard/FixTest.apk");
if (!file.exists()) {
Log.i("MApplication", "插件包不在");
return;
}
try {
//獲取到 BaseDexClassLoader 的 pathList字段
// private final DexPathList pathList;
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
//破壞封裝丢早,設(shè)置為可以調(diào)用
pathListField.setAccessible(true);
//拿到當(dāng)前ClassLoader的pathList對象
Object pathListObj = pathListField.get(getClassLoader());
//獲取當(dāng)前ClassLoader的pathList對象的字節(jié)碼文件(DexPathList )
Class<?> dexPathListClass = pathListObj.getClass();
//拿到DexPathList 的 dexElements字段
// private final Element[] dexElements姨裸;
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
//破壞封裝,設(shè)置為可以調(diào)用
dexElementsField.setAccessible(true);
//使用插件創(chuàng)建 ClassLoader
DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
//拿到插件的DexClassLoader 的 pathList對象
Object newPathListObj = pathListField.get(pathClassLoader);
//拿到插件的pathList對象的 dexElements變量
Object newDexElementsObj = dexElementsField.get(newPathListObj);
//將插件的 dexElements對象設(shè)置給 當(dāng)前ClassLoader的pathList對象
dexElementsField.set(pathListObj, newDexElementsObj);
} catch (Exception e) {
e.printStackTrace();
}
}
總結(jié)
思路還是很清晰的怨酝,主要是要先了解類加載的原理傀缩,整體來講還是比較簡單的;采用類加載方案的主要是以騰訊系為主农猬,包括微信的Tinker赡艰、QQ空間的超級補(bǔ)丁、手機(jī)QQ的QFix斤葱、餓了么的Amigo和Nuwa等等慷垮;也有一些其他的方法來實現(xiàn)熱修復(fù),有空再進(jìn)行總結(jié)分享揍堕。
Github地址: https://github.com/tyhjh/HotFix