代码:tinker 1.9.14.7
一、加载dex补丁
TinkerLoader.tryLoadPatchFilesInternal 会执行TinkerDexLoader.loadTinkerJars,此处开始加载dex补丁。
/**
* Load tinker JARs and add them to
* the Application ClassLoader.
*
* @param application The application.
*/
public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA, boolean isProtectedApp) {
if (LOAD_DEX_LIST.isEmpty() && classNDexInfo.isEmpty()) {
Log.w(TAG, "there is no dex to load");
return true;
}
//dalvik.system.PathClassLoader
ClassLoader classLoader = TinkerDexLoader.class.getClassLoader();
if (classLoader != null) {
Log.i(TAG, "classloader: " + classLoader.toString());
} else {
Log.e(TAG, "classloader is null");
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL);
return false;
}
// /data/user/0/com.stan.tinkersdkdemo/tinker/patch-f9dfd7d4/dex/tinker_classN.apk
String dexPath = directory + "/" + DEX_PATH + "/";
ArrayList legalFiles = new ArrayList<>();
for (ShareDexDiffPatchInfo info : LOAD_DEX_LIST) {
//for dalvik, ignore art support dex
if (isJustArtSupportDex(info)) {
continue;
}
String path = dexPath + info.realName;
File file = new File(path);
//对每个dex进行md5校验
if (application.isTinkerLoadVerifyFlag()) {
long start = System.currentTimeMillis();
String checkMd5 = getInfoMd5(info);
if (!SharePatchFileUtil.verifyDexFileMd5(file, checkMd5)) {
//it is good to delete the mismatch file
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
file.getAbsolutePath());
return false;
}
Log.i(TAG, "verify dex file:" + file.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
}
legalFiles.add(file);
}
//对apk进行md5校验
// verify merge classN.apk
if (isVmArt && !classNDexInfo.isEmpty()) {
File classNFile = new File(dexPath + ShareConstants.CLASS_N_APK_NAME);
long start = System.currentTimeMillis();
if (application.isTinkerLoadVerifyFlag()) {
for (ShareDexDiffPatchInfo info : classNDexInfo) {
if (!SharePatchFileUtil.verifyDexFileMd5(classNFile, info.rawName, info.destMd5InArt)) {
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
classNFile.getAbsolutePath());
return false;
}
}
}
//verify dex file:/data/user/0/com.stan.tinkersdkdemo/tinker/patch-f9dfd7d4/dex/tinker_classN.apk md5, use time: 0
Log.i(TAG, "verify dex file:" + classNFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
legalFiles.add(classNFile);
}
File optimizeDir = new File(directory + "/" + oatDir);
//如果系统ota升级了,删除oat文件,重新编译
if (isSystemOTA) {
final boolean[] parallelOTAResult = {true};
final Throwable[] parallelOTAThrowable = new Throwable[1];
String targetISA;
try {
targetISA = ShareTinkerInternals.getCurrentInstructionSet();
} catch (Throwable throwable) {
Log.i(TAG, "getCurrentInstructionSet fail:" + throwable);
// try {
// targetISA = ShareOatUtil.getOatFileInstructionSet(testOptDexFile);
// } catch (Throwable throwable) {
// don't ota on the front
deleteOutOfDateOATFile(directory);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, throwable);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_GET_OTA_INSTRUCTION_SET_EXCEPTION);
return false;
// }
}
//删除原来的oat文件夹
deleteOutOfDateOATFile(directory);
Log.w(TAG, "systemOTA, try parallel oat dexes, targetISA:" + targetISA);
// change dir
optimizeDir = new File(directory + "/" + INTERPRET_DEX_OPTIMIZE_PATH);
//触发dex2oat编译
TinkerDexOptimizer.optimizeAll(
application, legalFiles, optimizeDir, true, targetISA,
new TinkerDexOptimizer.ResultCallback() {
long start;
@Override
public void onStart(File dexFile, File optimizedDir) {
start = System.currentTimeMillis();
Log.i(TAG, "start to optimize dex:" + dexFile.getPath());
}
@Override
public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
// Do nothing.
Log.i(TAG, "success to optimize dex " + dexFile.getPath() + ", use time " + (System.currentTimeMillis() - start));
}
@Override
public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
parallelOTAResult[0] = false;
parallelOTAThrowable[0] = thr;
Log.i(TAG, "fail to optimize dex " + dexFile.getPath() + ", use time " + (System.currentTimeMillis() - start));
}
}
);
if (!parallelOTAResult[0]) {
Log.e(TAG, "parallel oat dexes failed");
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, parallelOTAThrowable[0]);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_OTA_INTERPRET_ONLY_EXCEPTION);
return false;
}
}
try {
//加载dex
SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles, isProtectedApp);
} catch (Throwable e) {
Log.e(TAG, "install dexes failed");
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
return false;
}
return true;
}
流程总结:
- 对/data/user/0/com.stan.tinkersdkdemo/tinker/patch-f9dfd7d4/dex/ 路径下的dex和tinker_classN.apk 进行md5验证
- 如果系统ota升级了,删除oat文件,重新编译TinkerDexOptimizer.optimizeAll
- 加载dex SystemClassLoaderAdder.installDexes
二、OTA升级之后重新编译
TinkerDexOptimizer.java
public static boolean optimizeAll(Context context, Collection dexFiles, File optimizedDir,
boolean useInterpretMode, String targetISA, ResultCallback cb) {
ArrayList sortList = new ArrayList<>(dexFiles);
// sort input dexFiles with its file length in reverse order.
//按文件大小进行排序
Collections.sort(sortList, new Comparator() {
@Override
public int compare(File lhs, File rhs) {
final long lhsSize = lhs.length();
final long rhsSize = rhs.length();
if (lhsSize < rhsSize) {
return 1;
} else if (lhsSize == rhsSize) {
return 0;
} else {
return -1;
}
}
});
//起线程OptimizeWorker对每个dexFile进行dex2oat编译
for (File dexFile : sortList) {
OptimizeWorker worker = new OptimizeWorker(context, dexFile, optimizedDir, useInterpretMode, targetISA, cb);
if (!worker.run()) {
return false;
}
}
return true;
}
按文件大小进行排序,起线程OptimizeWorker对每个dexFile进行dex2oat编译。
TinkerDexOptimizer.java
boolean run() {
try {
if (!SharePatchFileUtil.isLegalFile(dexFile)) {
if (callback != null) {
callback.onFailed(dexFile, optimizedDir,
new IOException("dex file " + dexFile.getAbsolutePath() + " is not exist!"));
return false;
}
}
if (callback != null) {
callback.onStart(dexFile, optimizedDir);
}
String optimizedPath = SharePatchFileUtil.optimizedPathFor(this.dexFile, this.optimizedDir);
if (!ShareTinkerInternals.isArkHotRuning()) {
//useInterpretMode初始化时是false
if (useInterpretMode) {
interpretDex2Oat(dexFile.getAbsolutePath(), optimizedPath);
} else if (Build.VERSION.SDK_INT >= 26
|| (Build.VERSION.SDK_INT >= 25 && Build.VERSION.PREVIEW_SDK_INT != 0)) {
NewClassLoaderInjector.triggerDex2Oat(context, optimizedDir, dexFile.getAbsolutePath());
} else {
DexFile.loadDex(dexFile.getAbsolutePath(), optimizedPath, 0);
}
}
if (callback != null) {
callback.onSuccess(dexFile, optimizedDir, new File(optimizedPath));
}
} catch (final Throwable e) {
Log.e(TAG, "Failed to optimize dex: " + dexFile.getAbsolutePath(), e);
if (callback != null) {
callback.onFailed(dexFile, optimizedDir, e);
return false;
}
}
return true;
}
android 7.1.1及之后的版本走NewClassLoaderInjector.triggerDex2Oat,低版本的直接走 DexFile.loadDex,DexFile.loadDex这个很明显,我前面文章讲过就是加载dex,后续再补充,先看高版本的triggerDex2Oat。
NewClassLoaderInjector.java
public static void triggerDex2Oat(Context context, File dexOptDir, String... dexPaths) throws Throwable {
// Suggestion from Huawei: Only PathClassLoader (Perhaps other ClassLoaders known by system
// like DexClassLoader also works ?) can be used here to trigger dex2oat so that JIT
// mechanism can participate in runtime Dex optimization.
final StringBuilder sb = new StringBuilder();
boolean isFirst = true;
for (String dexPath : dexPaths) {
if (isFirst) {
isFirst = false;
} else {
sb.append(File.pathSeparator);
}
sb.append(dexPath);
}
final ClassLoader triggerClassLoader = new PathClassLoader(sb.toString(), ClassLoader.getSystemClassLoader());
}
这里常规来说,DexClassLoader和PatchClassLoader应该都可以加载外部dex,而且DexClassLoader 还可以指定 optimizedDirectory,这里使用PatchClassLoader的唯一原因是华为做了限制。反正小米我之前做的时候没这讲究。
那么还有一个疑问?为什么用高版本用PathClassLoader加载 低版本用 DexFile.loadDex ?PathClassLoader最终也会调用DexFile.loadDex,想来想去,唯一解释是上报:
Android 8.0
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
if (reporter != null) {
reporter.report(this.pathList.getDexPaths());
}
}
Android 5.0
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
7.1.1及其以上版本,PatchClassLoader初始化时会上报tinker apk插件到DexManager,这样才会在idle&charge时由系统统一做一次speed-profile编译。但是Q以下的版本DexFile.loadDex对于过期的odex都会重新编译,所以唯一解释是,这里高版本使用PatchClassLoader只是兼容Q的DexFile.loadDex只走解释模式不做编译。
1.9.6版本就没有做区分,统一由DexFile.loadDex编译
public boolean run() {
try {
if (!SharePatchFileUtil.isLegalFile(this.dexFile) && this.callback != null) {
this.callback.onFailed(this.dexFile, this.optimizedDir, new IOException("dex file " + this.dexFile.getAbsolutePath() + " is not exist!"));
return false;
}
if (this.callback != null) {
this.callback.onStart(this.dexFile, this.optimizedDir);
}
String optimizedPath = SharePatchFileUtil.optimizedPathFor(this.dexFile, this.optimizedDir);
if (this.useInterpretMode) {
this.interpretDex2Oat(this.dexFile.getAbsolutePath(), optimizedPath);
} else {
DexFile.loadDex(this.dexFile.getAbsolutePath(), optimizedPath, 0);
}
if (this.callback != null) {
this.callback.onSuccess(this.dexFile, this.optimizedDir, new File(optimizedPath));
}
} catch (Throwable var2) {
Log.e("Tinker.ParallelDex", "Failed to optimize dex: " + this.dexFile.getAbsolutePath(), var2);
if (this.callback != null) {
this.callback.onFailed(this.dexFile, this.optimizedDir, var2);
return false;
}
}
return true;
}
三、加载dex
SystemClassLoaderAdder.java
public static void installDexes(Application application, ClassLoader loader, File dexOptDir, List files, boolean isProtectedApp)
throws Throwable {
Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());
if (!files.isEmpty()) {
files = createSortedAdditionalPathEntries(files);
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {
classLoader = NewClassLoaderInjector.inject(application, loader, dexOptDir, files);
} else {
//because in dalvik, if inner class is not the same classloader with it wrapper class.
//it won't fail at dex2opt
if (Build.VERSION.SDK_INT >= 23) {
V23.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 19) {
V19.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(classLoader, files, dexOptDir);
} else {
V4.install(classLoader, files, dexOptDir);
}
}
//install done
sPatchDexCount = files.size();
Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
if (!checkDexInstall(classLoader)) {
//reset patch dex
SystemClassLoaderAdder.uninstallPatchDex(classLoader);
throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
}
}
}
这里兼容了V4、V14、 V19、V23这么多个版本。看个V23
V23:
private static void install(ClassLoader loader, List additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
* file entries.
*/
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList suppressedExceptions = new ArrayList();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
new ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makePathElement", e);
throw e;
}
}
}
首先反射拿到反射得到 PathClassLoader 中的 pathList 对象,再将补丁文件通过反射调用makeDexElements 得到补丁文件的 Element[] ,再将补丁包的 Element[] 数组插入到 dexElements 中且在最前面,这样修复dex会在原来dex之前被加载,修复成功,原来dex失效。
最后,因为Android 7.0 及以上搞了混合编译,具体看 Android N混合编译与对热补丁影响解析
加载补丁操作做好之后,最后还要检查一下,如果没加载成功就会执行卸载:
if (!checkDexInstall(classLoader)) {
//reset patch dex
SystemClassLoaderAdder.uninstallPatchDex(classLoader);
throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
}