前言
在一個多月前,我寫過一篇熱修復(fù)初探桨嫁,主要介紹了各種被廣泛討論和使用的熱修復(fù)的技術(shù)實現(xiàn)原理强法,在那篇文章中,我也說自己會繼續(xù)研究基于dex分包的熱修復(fù)技術(shù)的源碼膏萧。
基于dex分包的熱修復(fù)技術(shù)應(yīng)該是QQ空間團隊最先提出來的漓骚,可是他們只是通過技術(shù)文章分享了實現(xiàn)原理,其本身的源碼并沒有公開榛泛,所以QQ的熱修復(fù)實現(xiàn)細節(jié)以及編碼風(fēng)格是沒有機會觀摩了蝌蹂,但是還是有很多團隊基于QQ空間介紹的原理實現(xiàn)了熱修復(fù)并且公開了源碼,比如@dodola大神的RocooFix和AnoleFix(沒錯曹锨,他弄了倆)叉信,還有一個是在餓了么工作的Android前輩開發(fā)的Amigo。
因為這位前輩特意在我的熱修復(fù)初探這篇文章下面留言向我宣傳他的框架艘希,所以首先我想來分析他的熱修復(fù)實現(xiàn)細節(jié)硼身。不過他自己也已經(jīng)寫了源碼解讀,雖然由于目前的代碼的更新導(dǎo)致他的源碼解讀和源碼有部分差異,但總體來說邏輯是一致的覆享。所以實際上我沒有必要在這里詳細的分析他的框架佳遂,只挑主要的來講。
Amigo熱修復(fù)框架剖析
Amigo github: https://github.com/eleme/Amigo
總得來說撒顿,從我看代碼的情況來看丑罪,這是一個比較完備的,可以應(yīng)用的熱修復(fù)框架凤壁,從檢測apk,到取出資源文件吩屹,dex文件,再到插入dex包到dexElements中拧抖,在重啟apk一系列過程都比較完善煤搜,考慮周到。所以唧席,在這里我只想講一件Amigo具體是如何將dex插入到dexElements中的擦盾,因為這個才是基于dex分包的熱修復(fù)技術(shù)的關(guān)鍵,不過他的修復(fù)方式和QQ空間團隊提出的de還是有一點不同淌哟。
Amigo.java
@Override
public void onCreate() {
super.onCreate();
......
......
......
Log.e(TAG, "demoAPk.exists-->" + demoAPk.exists() + ", this--->" + this);
ClassLoader originalClassLoader = getClassLoader();
try {
SharedPreferences sp = getSharedPreferences(SP_NAME, MODE_MULTI_PROCESS);
if (checkUpgrade(sp)) {
Log.e(TAG, "upgraded host app");
clear(this);
runOriginalApplication(originalClassLoader);
return;
}
if (!demoAPk.exists()) {
Log.e(TAG, "demoApk not exist");
clear(this);
runOriginalApplication(originalClassLoader);
return;
}
if (!isSignatureRight(this, demoAPk)) {
Log.e(TAG, "signature is illegal");
clear(this);
runOriginalApplication(originalClassLoader);
return;
}
if (!checkPatchApkVersion(this, demoAPk)) {
Log.e(TAG, "patch apk version cannot be less than host apk");
clear(this);
runOriginalApplication(originalClassLoader);
return;
}
if (!ProcessUtils.isMainProcess(this) && isPatchApkFirstRun(sp)) {
Log.e(TAG, "none main process and patch apk is not released yet");
runOriginalApplication(originalClassLoader);
return;
}
// only release loaded apk in the main process
runPatchApk(sp); //這是最重要的一句話
......
......
......
}
在Amigo這個類的onCreate方法里調(diào)用了runPatchApk(),開始準(zhǔn)備替換apk.再查看這個runPatchApk()方法
private void runPatchApk(SharedPreferences sp) throws LoadPatchApkException {
try {
String demoApkChecksum = getCrc(demoAPk);
boolean isFirstRun = isPatchApkFirstRun(sp);
Log.e(TAG, "demoApkChecksum-->" + demoApkChecksum + ", sig--->" + sp.getString(NEW_APK_SIG, ""));
if (isFirstRun) {
//clear previous working dir
Amigo.clearWithoutApk(this);
//start a new process to handle time-tense operation
ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), GET_META_DATA);
String layoutName = appInfo.metaData.getString("amigo_layout");
String themeName = appInfo.metaData.getString("amigo_theme");
int layoutId = 0;
int themeId = 0;
if (!TextUtils.isEmpty(layoutName)) {
layoutId = (int) readStaticField(Class.forName(getPackageName() + ".R$layout"), layoutName);
}
if (!TextUtils.isEmpty(themeName)) {
themeId = (int) readStaticField(Class.forName(getPackageName() + ".R$style"), themeName);
}
Log.e(TAG, String.format("layoutName-->%s, themeName-->%s", layoutName, themeName));
Log.e(TAG, String.format("layoutId-->%d, themeId-->%d", layoutId, themeId));
ApkReleaser.work(this, layoutId, themeId);
Log.e(TAG, "release apk once");
} else {
checkDexAndSoChecksum();
}
//創(chuàng)建一個繼承自PathClassLoader的類的對象迹卢,把補丁APK的路徑傳入構(gòu)造一個加載器
AmigoClassLoader amigoClassLoader = new AmigoClassLoader(demoAPk.getAbsolutePath(), getRootClassLoader());
//這個方法是將該app所對應(yīng)的ActivityThread對象中LoadApk的加載器通過反射的方式替換掉。
setAPKClassLoader(amigoClassLoader);
//這個就是準(zhǔn)備替換dex的方法
setDexElements(amigoClassLoader);
//顧名思義徒仓,設(shè)置加載本地庫
setNativeLibraryDirectories(amigoClassLoader);
//下面是加載一些資源文件
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = getDeclaredMethod(AssetManager.class, "addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, demoAPk.getAbsolutePath());
setAPKResources(assetManager);
runOriginalApplication(amigoClassLoader);
} catch (Exception e) {
throw new LoadPatchApkException(e);
}
}
在此腐碱,我們先不進入setDexElements(amigoClassLoader)這個方法,先看看設(shè)置類加載器的setAPKClassLoader(amigoClassLoader)方法掉弛,因為這也是很難忽略的一個關(guān)鍵點症见,因此喂走,我們先看看他是怎么設(shè)置加載器的
private void setAPKClassLoader(ClassLoader classLoader)
throws IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InvocationTargetException {
//把getLoadedApk()返回的對象中“mClassLoader”屬性替換成我們剛才自己new的類加載器
writeField(getLoadedApk(), "mClassLoader", classLoader);
}
writeFiled這個方法的主要功能就是通過反射的機制,把我們的classloader設(shè)置到mClassLoader中去筒饰,關(guān)鍵是getLoadedApk()到底是什么鬼?
private static Object getLoadedApk()
throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException {
//instance()返回一個“android.app.ActivityThread”類壁晒,readField是讀取ActivityThread類中的mPackages屬性
Map<String, WeakReference<Object>> mPackages = (Map<String, WeakReference<Object>>) readField(instance(), "mPackages", true);
//而這個mPackage屬性中包含有一個LoadedApk
for (String s : mPackages.keySet()) {
WeakReference wr = mPackages.get(s);
if (wr != null && wr.get() != null) {
//最終應(yīng)該返回了一個LoadedApk
return wr.get();
}
}
return null;
}
好了瓷们,最終得到了LoadedApk對象,這個對象其實很重要秒咐,一個 apk加載之后所有信息都保存在此對象(比如:DexClassLoader谬晕、Resources、Application)携取,一個包對應(yīng)一個對象攒钳,以包名區(qū)別,而我們正好就用我們自己的類加載器對象替換掉這個LoadedApk對象中的classloader,就可以加載我們自己的apk了雷滋。由于我們自己的amigoClassLoader實際上繼承自PathClassLoader不撑,所以智能加載特定目錄下的apk,也就是說,我們的補丁apk需要放在特定目錄下才行晤斩。
好了焕檬,扯了這么遠,我們還是趕緊回到正題澳泵,替換dex實現(xiàn)熱修復(fù)实愚。繼續(xù)從setDexElements(amigoClassLoader)往下走
private void setDexElements(ClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException {
//getPathList這是通過反射的方式去讀取BaseDexClassLoader中的pathList對象,這個對象中有一個dexElements數(shù)組兔辅,包裹了運行的APK中的所有的dex腊敲。
Object dexPathList = getPathList(classLoader);
//文件目錄下,補丁apk的dex文件對象數(shù)組
File[] listFiles = dexDir.listFiles();
List<File> validDexes = new ArrayList<>();
for (File listFile : listFiles) {
if (listFile.getName().endsWith(".dex")) {
//添加到列表中
validDexes.add(listFile);
}
}
//創(chuàng)建一個一樣大的文件數(shù)組
File[] dexes = validDexes.toArray(new File[validDexes.size()]);
//通過反射讀取dexPathList對象中的原本的dexElements數(shù)組對象
Object originDexElements = readField(dexPathList, "dexElements");
//返回dexElements數(shù)組中元素的類型
Class<?> localClass = originDexElements.getClass().getComponentType();
int length = dexes.length;
//然后根據(jù)這個類型創(chuàng)建一個同樣大的新數(shù)組
Object dexElements = Array.newInstance(localClass, length);
for (int k = 0; k < length; k++) {
為數(shù)組賦值
Array.set(dexElements, k, getElementWithDex(dexes[k], optimizedDir));
}
//最后维苔,通過反射的方式把這個新數(shù)組放到dexPathList這個對象中去碰辅。
writeField(dexPathList, "dexElements", dexElements);
}
好了,現(xiàn)在對于dex的替換基本上完成了介时,最后是一些重啟或者重新運行Application的工作乎赴。假如對于BaseDexClassLoader,dexPathList潮尝,dexElements這些還不是很清楚榕吼,可以看一看我之前的那篇文章熱修復(fù)初探,里面有相關(guān)的介紹。
小結(jié)
如果你真的認真看了我的上一篇文章熱修復(fù)初探的話勉失,你會發(fā)現(xiàn)這個框架其實跟我介紹了那種基于dex分包的熱修復(fù)原理還有一些出入羹蚣,因為這是整體把所有的dex包的替換掉,也就意味著當(dāng)需要熱修復(fù)時乱凿,下載的文件要大一些顽素,可能是整個apk咽弦;其次,這個框架使用的類加載器是PathClassLoader而不是DexClassLoader胁出,本來PathClassLoader是有局限的型型,因為它只能加載指定的私有路徑,而作者通過大量使用了反射的方式全蝶,直接替換原來的類加載器闹蒜,然后通過自己的類加載器來完成整個dex的完全替換∫忠總體來看绷落,這個框架除了體積較大,優(yōu)點是很多的始苇。(不過這么使用反射砌烁,APP應(yīng)該很難在Google play中上線吧?)
本來我工作中對于反射基本沒用到催式,所以算不上熟悉函喉,但是現(xiàn)在看來,這玩兒真的很好使啊荣月,因為用這種方式函似,可以獲取很多Android系統(tǒng)不公開的私有API和屬性......
臥槽,我決定好好研究反射,我發(fā)四。
勘誤
暫無
后記
本來還有繼續(xù)分析其他的熱修復(fù)框架源碼礁芦,但是這篇文章的篇幅已經(jīng)不小了,中場休息蔑担,找機會我再把其他的框架源碼的實現(xiàn)細節(jié)寫在新的文章中分享出來
最后是各個熱修復(fù)框架的性能表(不保證準(zhǔn)確)