前言
在一个多月前,我写过一篇热修复初探,主要介绍了各种被广泛讨论和使用的热修复的技术实现原理,在那篇文章中,我也说自己会继续研究基于dex分包的热修复技术的源码。
基于dex分包的热修复技术应该是QQ空间团队最先提出来的,可是他们只是通过技术文章分享了实现原理,其本身的源码并没有公开,所以QQ的热修复实现细节以及编码风格是没有机会观摩了,但是还是有很多团队基于QQ空间介绍的原理实现了热修复并且公开了源码,比如@dodola大神的RocooFix和AnoleFix(没错,他弄了俩),还有一个是在饿了么工作的Android前辈开发的Amigo。
因为这位前辈特意在我的热修复初探这篇文章下面留言向我宣传他的框架,所以首先我想来分析他的热修复实现细节。不过他自己也已经写了源码解读,虽然由于目前的代码的更新导致他的源码解读和源码有部分差异,但总体来说逻辑是一致的。所以实际上我没有必要在这里详细的分析他的框架,只挑主要的来讲。
Amigo热修复框架剖析
Amigo github: https://github.com/eleme/Amigo
总得来说,从我看代码的情况来看,这是一个比较完备的,可以应用的热修复框架,从检测apk,到取出资源文件,dex文件,再到插入dex包到dexElements中,在重启apk一系列过程都比较完善,考虑周到。所以,在这里我只想讲一件Amigo具体是如何将dex插入到dexElements中的,因为这个才是基于dex分包的热修复技术的关键,不过他的修复方式和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方法里调用了runPatchApk(),开始准备替换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();
}
//创建一个继承自PathClassLoader的类的对象,把补丁APK的路径传入构造一个加载器
AmigoClassLoader amigoClassLoader = new AmigoClassLoader(demoAPk.getAbsolutePath(), getRootClassLoader());
//这个方法是将该app所对应的ActivityThread对象中LoadApk的加载器通过反射的方式替换掉。
setAPKClassLoader(amigoClassLoader);
//这个就是准备替换dex的方法
setDexElements(amigoClassLoader);
//顾名思义,设置加载本地库
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)这个方法,先看看设置类加载器的setAPKClassLoader(amigoClassLoader)方法,因为这也是很难忽略的一个关键点,因此,我们先看看他是怎么设置加载器的
private void setAPKClassLoader(ClassLoader classLoader)
throws IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InvocationTargetException {
//把getLoadedApk()返回的对象中“mClassLoader”属性替换成我们刚才自己new的类加载器
writeField(getLoadedApk(), "mClassLoader", classLoader);
}
writeFiled这个方法的主要功能就是通过反射的机制,把我们的classloader设置到mClassLoader中去,关键是getLoadedApk()到底是什么鬼?
private static Object getLoadedApk()
throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException {
//instance()返回一个“android.app.ActivityThread”类,readField是读取ActivityThread类中的mPackages属性
Map> mPackages = (Map>) readField(instance(), "mPackages", true);
//而这个mPackage属性中包含有一个LoadedApk
for (String s : mPackages.keySet()) {
WeakReference wr = mPackages.get(s);
if (wr != null && wr.get() != null) {
//最终应该返回了一个LoadedApk
return wr.get();
}
}
return null;
}
好了,最终得到了LoadedApk对象,这个对象其实很重要,一个 apk加载之后所有信息都保存在此对象(比如:DexClassLoader、Resources、Application),一个包对应一个对象,以包名区别,而我们正好就用我们自己的类加载器对象替换掉这个LoadedApk对象中的classloader,就可以加载我们自己的apk了。由于我们自己的amigoClassLoader实际上继承自PathClassLoader,所以智能加载特定目录下的apk,也就是说,我们的补丁apk需要放在特定目录下才行。
好了,扯了这么远,我们还是赶紧回到正题,替换dex实现热修复。继续从setDexElements(amigoClassLoader)往下走
private void setDexElements(ClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException {
//getPathList这是通过反射的方式去读取BaseDexClassLoader中的pathList对象,这个对象中有一个dexElements数组,包裹了运行的APK中的所有的dex。
Object dexPathList = getPathList(classLoader);
//文件目录下,补丁apk的dex文件对象数组
File[] listFiles = dexDir.listFiles();
List validDexes = new ArrayList<>();
for (File listFile : listFiles) {
if (listFile.getName().endsWith(".dex")) {
//添加到列表中
validDexes.add(listFile);
}
}
//创建一个一样大的文件数组
File[] dexes = validDexes.toArray(new File[validDexes.size()]);
//通过反射读取dexPathList对象中的原本的dexElements数组对象
Object originDexElements = readField(dexPathList, "dexElements");
//返回dexElements数组中元素的类型
Class> localClass = originDexElements.getClass().getComponentType();
int length = dexes.length;
//然后根据这个类型创建一个同样大的新数组
Object dexElements = Array.newInstance(localClass, length);
for (int k = 0; k < length; k++) {
为数组赋值
Array.set(dexElements, k, getElementWithDex(dexes[k], optimizedDir));
}
//最后,通过反射的方式把这个新数组放到dexPathList这个对象中去。
writeField(dexPathList, "dexElements", dexElements);
}
好了,现在对于dex的替换基本上完成了,最后是一些重启或者重新运行Application的工作。假如对于BaseDexClassLoader,dexPathList,dexElements这些还不是很清楚,可以看一看我之前的那篇文章热修复初探,里面有相关的介绍。
小结
如果你真的认真看了我的上一篇文章热修复初探的话,你会发现这个框架其实跟我介绍了那种基于dex分包的热修复原理还有一些出入,因为这是整体把所有的dex包的替换掉,也就意味着当需要热修复时,下载的文件要大一些,可能是整个apk;其次,这个框架使用的类加载器是PathClassLoader而不是DexClassLoader,本来PathClassLoader是有局限的,因为它只能加载指定的私有路径,而作者通过大量使用了反射的方式,直接替换原来的类加载器,然后通过自己的类加载器来完成整个dex的完全替换。总体来看,这个框架除了体积较大,优点是很多的。(不过这么使用反射,APP应该很难在Google play中上线吧?)
本来我工作中对于反射基本没用到,所以算不上熟悉,但是现在看来,这玩儿真的很好使啊,因为用这种方式,可以获取很多Android系统不公开的私有API和属性......
卧槽,我决定好好研究反射,我发四。
勘误
暂无
后记
本来还有继续分析其他的热修复框架源码,但是这篇文章的篇幅已经不小了,中场休息,找机会我再把其他的框架源码的实现细节写在新的文章中分享出来
最后是各个热修复框架的性能表(不保证准确)