33. 热修复-QQ空间超级补丁方案-Android N兼容性
2021-02-10 本文已影响0人
任振铭
Android N混合编译
ART 是在 Android KitKat(Android 4.0)引入并在 Lollipop(Android 5.0)中设为默认运行环境,可以看作Dalvik2.0。 ART模式在Android N(7.0)之前安装APK时会采用AOT(Ahead of time:提前编译、静态编译)预编译为机器码。
而在Android N使用混合模式的运行时。应用在安装时不做编译,而是运行时解释字节码,同时在JIT编译了一 些代码后将这些代码信息记录至Profile文件,等到设备空闲的时候使用AOT(All-Of-the-Time compilation:全 时段编译)编译生成称为app_image的base.art(类对象映像)文件,这个art文件会在apk启动时自动加载(相当 于缓存)。也就是说这种情况下,当我们的application的onCreate方法执行的时候,可能需要被修复的类已经被加载过了,根据类加载原理,类被加载了无法被替换,即无法修复。
Tinker团队 Android N混合编译与对热补丁影响解析
可以看下tinker对这个问题的描述
屏幕快照 2021-02-10 上午10.04.23.png
屏幕快照 2021-02-10 上午10.04.50.png
屏幕快照 2021-02-10 上午10.05.18.png
那么既然存在这样的问题,解决的办法是什么?就是运行时替换PathClassLoader方案
运行时替换PathClassLoader方案
App image中的class是插入到PathClassloader中的ClassTable中。假设我们完全废弃掉PathClassloader,而 采用一个新建Classloader来加载后续的所有类,即可达到将cache无用化的效果。
这种方式不会影响没有补丁时的性能,但在加载补丁后,由于废弃了App image带来一定的性能损耗。具体数据如下:
取自微信技术文档2021-02-10 上午10.11.18.png
代码实现
1.新建ClassLoader
/**
* 1、先把补丁包的dex拼起来
*/
// 获得原始的dexPath用于构造classloader
StringBuilder dexPathBuilder = new StringBuilder();
String packageName = context.getPackageName();
boolean isFirstItem = true;
for (File patch : patchs) {
//添加:分隔符 /xx/a.dex:/xx/b.dex
if (isFirstItem) {
isFirstItem = false;
} else {
dexPathBuilder.append(File.pathSeparator);
}
dexPathBuilder.append(patch.getAbsolutePath());
}
/**
* 2、把apk中的dex拼起来
*/
//得到原本的pathList
Field pathListField = ShareReflectUtil.findField(oldClassLoader, "pathList");
Object oldPathList = pathListField.get(oldClassLoader);
//dexElements
Field dexElementsField = ShareReflectUtil.findField(oldPathList, "dexElements");
Object[] oldDexElements = (Object[]) dexElementsField.get(oldPathList);
//从Element上得到 dexFile
Field dexFileField = ShareReflectUtil.findField(oldDexElements[0], "dexFile");
for (Object oldDexElement : oldDexElements) {
String dexPath = null;
DexFile dexFile = (DexFile) dexFileField.get(oldDexElement);
if (dexFile != null) {
dexPath = dexFile.getName();
}
if (dexPath == null || dexPath.isEmpty()) {
continue;
}
if (!dexPath.contains("/" + packageName)) {
continue;
}
if (isFirstItem) {
isFirstItem = false;
} else {
dexPathBuilder.append(File.pathSeparator);
}
dexPathBuilder.append(dexPath);
}
String combinedDexPath = dexPathBuilder.toString();
/**
* 3、获取apk中的so加载路径
*/
// app的native库(so) 文件目录 用于构造classloader
Field nativeLibraryDirectoriesField = ShareReflectUtil.findField(oldPathList, "nativeLibraryDirectories");
List<File> oldNativeLibraryDirectories = (List<File>) nativeLibraryDirectoriesField.get(oldPathList);
StringBuilder libraryPathBuilder = new StringBuilder();
isFirstItem = true;
for (File libDir : oldNativeLibraryDirectories) {
if (libDir == null) {
continue;
}
if (isFirstItem) {
isFirstItem = false;
} else {
libraryPathBuilder.append(File.pathSeparator);
}
libraryPathBuilder.append(libDir.getAbsolutePath());
}
String combinedLibraryPath = libraryPathBuilder.toString();
//创建自己的类加载器
ClassLoader result = new EnjoyClassLoader(combinedDexPath, combinedLibraryPath, ClassLoader.getSystemClassLoader());
2.替换ClassLoader
Thread.currentThread().setContextClassLoader(classLoader);
Context baseContext = (Context) ShareReflectUtil.findField(app, "mBase").get(app);
if (Build.VERSION.SDK_INT >= 26) {
ShareReflectUtil.findField(baseContext, "mClassLoader").set(baseContext, classLoader);
}
Object basePackageInfo = ShareReflectUtil.findField(baseContext, "mPackageInfo").get(baseContext);
ShareReflectUtil.findField(basePackageInfo, "mClassLoader").set(basePackageInfo, classLoader);
if (Build.VERSION.SDK_INT < 27) {
Resources res = app.getResources();
try {
ShareReflectUtil.findField(res, "mClassLoader").set(res, classLoader);
final Object drawableInflater = ShareReflectUtil.findField(res, "mDrawableInflater").get(res);
if (drawableInflater != null) {
ShareReflectUtil.findField(drawableInflater, "mClassLoader").set(drawableInflater, classLoader);
}
} catch (Throwable ignored) {
// Ignored.
}
}