前言
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<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) {
//最终应该返回了一个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<File> 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);
}
小结
本来我工作中对于反射基本没用到,所以算不上熟悉,但是现在看来,这玩儿真的很好使啊,因为用这种方式,可以获取很多Android系统不公开的私有API和属性......
卧槽,我决定好好研究反射,我发四。
勘误
暂无
后记
本来还有继续分析其他的热修复框架源码,但是这篇文章的篇幅已经不小了,中场休息,找机会我再把其他的框架源码的实现细节写在新的文章中分享出来
最后是各个热修复框架的性能表(不保证准确)