写点什么

你的 debug 包在 Android 14 变卡了吗?|得物技术

作者:得物技术
  • 2024-04-23
    上海
  • 本文字数:5770 字

    阅读完需:约 19 分钟

你的debug包在Android 14变卡了吗?|得物技术

一、背景

我的 App 怎么这么卡,谁在代码里下毒了!


有一天突然发现 debug 包运行变的特别卡顿,经过下面的简单测试发现 debug 包在 Android 14 上出了问题。



二、问题排查纪录

常规手段排查


使用了 systrace 以及内部的 debug 包 trace 工具 dutrace 进行排查。


结论:CPU 空闲,主线程无明显阻塞,看上去就是纯方法执行耗时。


发现怀疑点


第一步排查过程中没有特别大的收获,但是我用 dutrace 工具排查时发现了一个异常现象。这里简单介绍一下 dutrace 的实现原理:


dutrace 是利用 inline hook 在 artmethod 的执行前后加上 atrace 的点再通过 perfetto ui 工具展示。有以下优点:


1. 支持线下分析函数执行流程,函数耗时。


2. 在分析函数调用流程下:


a. 可以查看整个过程的函数调用(包括 framework 函数);


b. 能够指定监控的函数和线程有效过滤无用 trace;


c. 动态配置不需要重新打包。


3. 可使用现成的 UI 分析工具,有系统关键线程的函数调用,例如渲染耗时、线程锁,GC 耗时等,还有 I/O 操作、CPU 负载等事件。



在对 artmethod 执行前后进行 hook 时 这里涉及到处理 art 方法解释执行的三种情况。


ART Runtime 解释器


  1. The C++ interpreter,也就是传统的基于 switch 结构的解释器,一般仅在调试环境、方法跟踪、指令不支持或者在字节码发生异常情况下(例如 failed structured-locking verification)才走该分支。

  2. The mterp fast interpreter,核心是引入了 handler table 做指令映射,并通过手写汇编以实现指令间的快速切换,提高了解释器性能。

  3. Nterp 是 Mterp 的再次优化。Nterp 省去了 managed code stacks 的维护,采用了和 Native 方法一样的栈帧结构,并且译码和翻译执行全程都由汇编代码实现,进一步拉进解释器和 compiled code 的性能差距。


在这边我发现了一个异常现象,就是 Android 14 的解释执行居然都用的 switch 解释执行方式。我又重新去测试了几个 Android 版本的解释执行方式。Android 12 走的 mterp,Android 13 走的是 nterp,当进行调试的时候才会走到 switch, 理论上 Android 14 应该也走 nterp 才对,怎么会走了最慢的 switch 呢。以下按顺序是 12、13、14 版本的方法执行 backtrace。




排查怀疑点


开始怀疑是解释执行导致的卡顿了,翻了下源码 art/runtime/interpreter/mterp/nterp.cc 中确实有变动 如果是 javaDebuggable 就不走 nterp 了。接下来尝试去证明是是这个问题导致的。



isJavaDebuggable 是 runtime.cc 中的 RuntimeDebugState runtime_debug_state_ 中控制的。我们可以找到 runtime 的实例然后通过偏移量修改过 runtime_debug_state_属性,看了下源码还可以通过_ZN3art7Runtime20SetRuntimeDebugStateENS0_17RuntimeDebugStateE 进行设置。


void Runtime::SetRuntimeDebugState(RuntimeDebugState state) {   if (state != RuntimeDebugState::kJavaDebuggableAtInit) {         // We never change the state if we started as a debuggable runtime.         DCHECK(runtime_debug_state_ != RuntimeDebugState::kJavaDebuggableAtInit);     }     runtime_debug_state_ = state;}
复制代码


我通过上述方式去进行尝试验证 把测试包的 isJavaDebuggable 设置为 false 依然卡顿,把生产包的 isJavaDebuggable 设置为 true,变得稍微卡了点。于是我推翻了自己解释执行方式导致卡顿的猜想。


排查 native 耗时


怀疑 nativie 方法执行耗时, 再次尝试用 simpleperf 定位问题。


结论:基本都是解释执行代码中的堆栈耗时,没有其他特殊堆栈。



定位到 DEBUG_JAVA_DEBUGGABLE


那就想着从 debuggable 的源头入手,逐步缩小范围定位影响变量。


AndroidManifest 中的 debuggable 影响系统 system 进程启动我们进程中的一个 runtimeFlags。


frameworks/base/core/java/android/os/Process.java 中的 start 方法 其中第 6 个参数就是 runtimeFlags 而如果是 debuggableFlag runtimeFlags 会被添加以下一些 flag 那就先缩小标签范围。


 if (debuggableFlag) {                runtimeFlags |= Zygote.DEBUG_ENABLE_JDWP;                                runtimeFlags |= Zygote.DEBUG_ENABLE_PTRACE;                                runtimeFlags |= Zygote.DEBUG_JAVA_DEBUGGABLE;                                // Also turn on CheckJNI for debuggable apps. It's quite                                // awkward to turn on otherwise.                                runtimeFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;                                // Check if the developer does not want ART verification                                if (android.provider.Settings.Global.getInt(mService.mContext.getContentResolver(),                                                android.provider.Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE, 1) == 0) {                                        runtimeFlags |= Zygote.DISABLE_VERIFIER;                                        Slog.w(TAG_PROCESSES, app + ": ART verification disabled");                                 }                          }
复制代码


需要修改我们进程的启动参数。那就需要去 hook system 进程了。这边涉及到手机 root,安装 hook 框架的一些操作,然后通过 hook Process 的 start 去做一些参数修改。


hookAllMethods(        Process.class,                "start",                new XC_MethodHook() {                        @Override                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable {                                final String niceName = (String) param.args[1];                                final int uid = (int) param.args[2];                                final int runtimeFlags = (int) param.args[5];                                XposedBridge.log("process_xx " + runtimeFlags);                                if (isDebuggable(niceName, user)) {                                        param.args[5] = runtimeFlags&~DEBUG_JAVA_DEBUGGABLE;                                        XposedBridge.log("process_xx " + param.args[5]);                                }                         }                  });
复制代码


这次还是有一些明显的结果的。测试包 runtimeflags 移除 DEBUG_JAVA_DEBUGGABLE 后不卡了。而生产包包括应用市场上的应用加上 DEBUG_JAVA_DEBUGGABLE 标记后全部都变卡了。那就可以证明是 DEBUG_JAVA_DEBUGGABLE 这个变量引起的。


定位到


DeoptimizeBootImage


继续源码观察 DEBUG_JAVA_DEBUGGABLE 带来的影响。


if ((runtime_flags & DEBUG_JAVA_DEBUGGABLE) != 0) {        runtime->AddCompilerOption("--debuggable");        runtime_flags |= DEBUG_GENERATE_MINI_DEBUG_INFO;        runtime->SetRuntimeDebugState(Runtime::RuntimeDebugState::kJavaDebuggableAtInit);        {            // Deoptimize the boot image as it may be non-debuggable.            ScopedSuspendAll ssa(__FUNCTION__);            runtime->DeoptimizeBootImage();          }          runtime_flags &= ~DEBUG_JAVA_DEBUGGABLE;          needs_non_debuggable_classes = true;      }
复制代码


这里有逻辑是 DEBUG_JAVA_DEBUGGABLE 带来的影响点,SetRuntimeDebugState 之前已经测试过了。也不是 DEBUG_GENERATE_MINI_DEBUG_INFO 带来的影响,那是 runtime->DeoptimizeBootImage()?于是我用 debugable 为 false 的包通过_ZN3art7Runtime19DeoptimizeBootImageEv 主动去调用了 DeoptimizeBootImage 方法,然后复现了!


原因分析


DeoptimizeBootImage 将 bootImage 中 AOT 代码方法转换为 java 可调试。重新初始化方法入口点,走到解释执行,而不使用 AOT 代码。追溯到 Instrumentation::InitializeMethodsCode 方法,还是到了 CanUseNterp(method) CanRuntimeUseNterp 这个点。也是 Android 13 可以用 nterp,android 14 只能走 switch 了。


我再次 hook 代码,让 CanRuntimeUseNterp 直接 return true, 但是还是卡。我发现即使我 hook 了。下面的这些方法还是走到了 switch 解释执行。反过来想一想是因为我 hook 已经滞后了 DeoptimizeBootImage 已经执行了,当调用到基础方法的时候都是 switch 执行了。



我用 Android 13 debugable true 的包进行测试先 hook CanRuntimeUseNterp return false,然后再执行 DeoptimizeBootImage,复现卡顿 。


初步定位: bootimage 中的方法 Android 13 走的 nterp 而 Android 14 走的 switch  bootimage 里面的方法特别基础和零碎所以导致方法 switch 执行耗时严重。


验证是系统问题


如果是系统问题,那大家都应该遇到的,不只我们 App 有这个问题, 于是我找到了几个小伙伴帮忙验证 debug 包这个问题。果然都有这个问题,同一个包安装在 Android 14 和 Android 13 上体验完全不一致。


反馈问题


在 issuetracker 上已经有人反馈 android 14 debug 包慢了 https://issuetracker.google.com/issues/311251587。但是还没有结果,于是我补上了我定位到的问题。



顺便也提了个 issue https://issuetracker.google.com/issues/328477628

三、临时解决

在等 Google 回复的同时,也同时在思考 App 层可以有什么办法去规避这个问题,让 debug 包的体验也回归丝滑,比如如何去重新 optimize bootimage 中的方法。抱着这个想法又去学习了一下 art 的代码,发现 Android 14 新增了一个 UpdateEntrypointsForDebuggable 方法,这个方法会去按照规则重新设置方法的执行方式比如 aot 和 nterp,那我在这之前把 CanRuntimeUseNterp hook 了返回 true 再去调用 UpdateEntrypointsForDebuggable 不就会重新走到 nterp 了吗。


void Instrumentation::UpdateEntrypointsForDebuggable() {    Runtime* runtime = Runtime::Current();    // If we are transitioning from non-debuggable to debuggable, we patch    // entry points of methods to remove any aot / JITed entry points.    InstallStubsClassVisitor visitor(this);    runtime->GetClassLinker()->VisitClasses(&visitor);}
复制代码


按照上面的思路尝试了一波,果然变得流畅很多!!!


其实上面的解决方案还有遗留问题。对比 debugable 为 false 的包还是有些卡顿。我也发现了 bootImage 中的方法已经走到 nterp 上了,但是 apk 中的大部分代码还是走到了 switch 解释执行上,于是我改变思路。我在调用 UpdateEntrypointsForDebuggable 前先把 RuntimeDebugState 设置成非 debugable,调用之后再把 RuntimeDebugState 设置会 debugable 不就行了吗。最后的代码如下,hook 框架使用了https://github.com/bytedance/android-inline-hook


Java_test_ArtMethodTrace_bootImageNterp(JNIEnv *env,                                                                                                             jclass clazz) {        void *handler = shadowhook_dlopen("libart.so");        instance_ = static_cast<void **>(shadowhook_dlsym(handler, "_ZN3art7Runtime9instance_E"));        jobject        (*getSystemThreadGroup)(void *runtime) =(jobject (*)(void *runtime)) shadowhook_dlsym(handler,                                                                                                                                                                                    "_ZNK3art7Runtime20GetSystemThreadGroupEv");    void    (*UpdateEntrypointsForDebuggable)(void *instrumentation) = (void (*)(void *i)) shadowhook_dlsym(                        handler,             "_ZN3art15instrumentation15Instrumentation30UpdateEntrypointsForDebuggableEv");        if (getSystemThreadGroup == nullptr || UpdateEntrypointsForDebuggable == nullptr) {                LOGE("getSystemThreadGroup  failed ");                shadowhook_dlclose(handler);                return;     }         jobject thread_group = getSystemThreadGroup(*instance_);         int vm_offset = findOffset(*instance_, 0, 4000, thread_group);         if (vm_offset < 0) {                 LOGE("vm_offset not found ");                 shadowhook_dlclose(handler);                 return;          }          void (*setRuntimeDebugState)(void *instance_,                                    int r) =(void (*)(void *runtime,                                                                    int r)) shadowhook_dlsym(                         handler, "_ZN3art7Runtime20SetRuntimeDebugStateENS0_17RuntimeDebugStateE");          if (setRuntimeDebugState != nullptr) {                  setRuntimeDebugState(*instance_, 0);          }          void *instrumentation = reinterpret_cast<void *>(reinterpret_cast<char *>(*instance_) +                                                                                                            vm_offset - 368 );                                                             UpdateEntrypointsForDebuggable(instrumentation);          setRuntimeDebugState(*instance_, 2);          shadowhook_dlclose(handler);          LOGE("bootImageNterp success");      }
复制代码

四、最后

最近在社区上也看到了高通工程师的一篇文章,他在我定位到的问题的基础上做了更详细的分析,确认了 Google 会在 Android 15 上修复这个问题,如果是海外版本的 Android 14 设备,Google 计划通过 com.android.artapex 模块的更新来修复这个问题。但是国内由于网络的问题,Google 的推送无法工作,因此需要各个手机厂家来主动合入这两笔改动。[1]


如果大家需要临时解决 debugable 包的卡顿的问题也可以通过上述方式解决。


参考文章:


[1] https://juejin.cn/post/7353106089296789556


*文/ 乌柚


本文属得物技术原创,更多精彩文章请看:得物技术官网


未经得物技术许可严禁转载,否则依法追究法律责任!


发布于: 50 分钟前阅读数: 13
用户头像

得物技术

关注

得物APP技术部 2019-11-13 加入

关注微信公众号「得物技术」

评论

发布
暂无评论
你的debug包在Android 14变卡了吗?|得物技术_android_得物技术_InfoQ写作社区