【探讨】关于Tinker补丁成功的校验方法: SystemClassLoaderAdder#checkDexInstall
手机型号:小米10、华为等Android11+机型
手机系统版本:Android 12
tinker版本:v1.9.15.1
gradle版本:8.9
问题背景:在某次版本迭代需要使用Tinker热修复一个关于ButterKnife的类型转换的异常
热修复前
@BindView(R.id.root)
lateinit var root: ConstraintLayout
热修复后
@BindView(R.id.root)
lateinit var root: LinearLayout
然后对应布局也进行了一波调整
问题情况:
- 补丁生成 通过
- 补丁安装 通过
- 重启应用时,发现其抛出异常,如下
问题异常:
2024-12-10 19:46:40.601 6457-6457 Tinker.DefaultLoadReporter com.example.tinker E patch loadReporter onLoadException: tinker dex check fail:Tinker Exception:checkDexInstall failed
2024-12-10 19:46:40.604 6457-6457 Tinker.DefaultLoadReporter com.example.tinker I dex exception disable tinker forever with sp
2024-12-10 19:46:40.605 6457-6457 Tinker.DefaultLoadReporter com.example.tinker E tinker load exception, welcome to submit issue to us: https://github.com/Tencent/tinker/issues
2024-12-10 19:46:40.605 6457-6457 Tinker.DefaultLoadReporter com.example.tinker E tinker load exception com.tencent.tinker.loader.TinkerRuntimeException: Tinker Exception:checkDexInstall failed
at com.tencent.tinker.loader.SystemClassLoaderAdder.installDexes(SystemClassLoaderAdder.java:73)
at com.tencent.tinker.loader.TinkerDexLoader.loadTinkerJars(TinkerDexLoader.java:191)
at com.tencent.tinker.loader.TinkerLoader.tryLoadPatchFilesInternal(TinkerLoader.java:356)
at com.tencent.tinker.loader.TinkerLoader.tryLoad(TinkerLoader.java:57)
at java.lang.reflect.Method.invoke(Native Method)
at com.tencent.tinker.loader.app.TinkerApplication.loadTinker(TinkerApplication.java:126)
at com.tencent.tinker.loader.app.TinkerApplication.onBaseContextAttached(TinkerApplication.java:164)
at com.tencent.tinker.loader.app.TinkerApplication.attachBaseContext(TinkerApplication.java:187)
at android.app.Application.attach(Application.java:338)
at android.app.Instrumentation.newApplication(Instrumentation.java:1191)
at android.app.LoadedApk.makeApplication(LoadedApk.java:1546)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:8574)
at android.app.ActivityThread.access$2800(ActivityThread.java:313)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2912)
at android.os.Handler.dispatchMessage(Handler.java:117)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:293)
at android.app.ActivityThread.loopProcess(ActivityThread.java:9986)
at android.app.ActivityThread.main(ActivityThread.java:9975)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:586)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1240)
在排查问题的过程中,也发现了一部分相似的问题:
- #1727
- #1699
- #1566
本质上述问题的原因,都是出现SystemClassLoaderAdder#checkDexInstall 这个函数
深入源码:
SystemClassLoaderAdder#installDexes
public static void installDexes(Application application, ClassLoader loader, File dexOptDir, List<File> files,
boolean isProtectedApp, boolean useDLC) throws Throwable {
ShareTinkerLog.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, useDLC, files);
} else {
injectDexesInternal(classLoader, files, dexOptDir);
}
//install done
sPatchDexCount = files.size();
ShareTinkerLog.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);
}
}
}
SystemClassLoaderAdder#checkDexInstall
private static boolean checkDexInstall(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Class<?> clazz = Class.forName(CHECK_DEX_CLASS, true, classLoader);
Field filed = ShareReflectUtil.findField(clazz, CHECK_DEX_FIELD);
boolean isPatch = (boolean) filed.get(null);
ShareTinkerLog.i(TAG, "checkDexInstall result: %s, checker_classloader: %s", isPatch, clazz.getClassLoader());
return isPatch;
}
整体流程没有大的问题,对于补丁合成的流程的校验也是OK的
深入查看产物:
- 查看生成的Patch文件
其TinkerTestDexLoad#isPatch的值为true
- 查看Tinker的合成补丁之后的产物 tinker_classN
发现其TinkerTestDexLoad#isPatch的值为false
- 问题初步确定 1.合成之后的产物TinkerTestDexLoad#isPatch为false,导致校验失败 2.往上进一步分析,是基准包在打包的时候已经包含TinkerTestDexLoad#isPatch为false、而补丁包的TinkerTestDexLoad#isPatch虽然为true,classLoader在加载时会优先使用前面的classes.dex文件,才导致问题产生
解决方式:
- 打包的时候,将TinkerTestDexLoad#isPatch移除
- 根据目前的理解,如果补丁生成以及合成校验都没问题的话,直接找到补丁包的TinkerTestDexLoad#isPatch为true,同样也说明补丁合成流程没问题
我根据这个问题,大部分的Tinker的源码我都查阅了。我发现TinkerTestDexLoad#isPatch这块的流程很早版本之前就存在了
同样我也找到,为什么加载这个标志的原因
这里我有部分疑问,需要各位朋友帮忙解答一下:
-
TinkerTestDexLoad#isPatch相关的代码在16年就存在了,但是当时热修复使用几乎没有此类问题,为什么在Android 11 之后此类问题却频繁出现?这个问题更深层次的原因到底是由于哪块问题导致的?
-
上面判定出问题原因是合成之后的TinkerTestDexLoad#isPatch重复导致的,还有没有更深层次的原因?
-
上面的修复方式,虽然解决了目前问题,但是不确定是不是一个好的修复方式?
我看后续Tinker的小的适配部分都是您这边在维护,麻烦抽空帮忙看看这个问题,感谢 @tys282000
Solution:
- When packaging, remove TinkerTestDexLoad#isPatch
- According to the current understanding, if there is no problem with patch generation and synthesis verification, directly find the patch package TinkerTestDexLoad#isPatch is true, which also indicates that the patch synthesis process is fine.
Can you elaborate on the steps you took to fix this issue in your app? I am also facing this. Thank you! @WanDa1993
@gergesh At that time, I was testing for that issue.
Although the experimental results were as expected, I did not choose this method for repair.
Because we were unsure if this repair would cause other problems, we ultimately chose not to use a hot fix and opted for a more stable release version patch.
I documented this scenario, and I might only try this approach in emergency and necessary situations.
@WanDa1993 How can i set this isPatch variable to true while packing?
Thanks.