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對這個問題的描述
那麼既然存在這樣的問題,解決的辦法是什麼?就是運行時替換PathClassLoader方案
運行時替換PathClassLoader方案
App image中的class是插入到PathClassloader中的ClassTable中。假設我們完全廢棄掉PathClassloader,而 採用一個新建Classloader來加載後續的所有類,即可達到將cache無用化的效果。
這種方式不會影響沒有補丁時的性能,但在加載補丁後,由於廢棄了App image帶來一定的性能損耗。具體數據如下:
代碼實現
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.
}
}