你的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 解释器
- The C++ interpreter,也就是传统的基于switch结构的解释器,一般仅在调试环境、方法跟踪、指令不支持或者在字节码发生异常情况下(例如failed structured-locking verification)才走该分支。
- The mterp fast interpreter,核心是引入了handler table做指令映射,并通过手写汇编以实现指令间的快速切换,提高了解释器性能。
- 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
*文/乌柚
本文属得物技术原创,更多精彩文章请看:得物技术官网
未经得物技术许可严禁转载,否则依法追究法律责任!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Databend 开源周报第 141 期
Databend 是一款现代云数仓。专为弹性和高效设计,为您的大规模分析需求保驾护航。自由且开源。即刻体验云服务:https://app.databend.cn 。 What's On In Databend 探索 Databend 本周新进展,遇到更贴近你心意的 Databend 。 全文检索能力进阶 在近期的更新中,Databend 进一步增强了全文检索能力,对 match 和 query 函数进行了扩展和优化,使得用户能够根据复杂的需求进行精确的搜索和数据检索。 主要更新如下: 多字段加权支持: match 函数现在支持对多个字段进行加权,可以根据不同字段的重要性赋予不同的权重。 增强的查询语法: query 函数引入了一系列新的查询语法,使得搜索更加灵活和强大。具体支持的查询语法包括: 简单搜索:允许直接通过字段进行搜索,例如 title:quick 。 布尔运算符:支持使用 AND、OR 逻辑运算符进行复合查询,例如 title:fox AND dog OR cat 。 必须和否定运算符:通过使用 + 和 - 运算符指定某些词必须存在或必须排除,例如 title:+fox -...
- 下一篇
🎉Laravel + Vue3 前后端分离后端框架 CatchAdmin v3.2.3 发布
介绍 CatchAdmin是一款基于Laravel和Element Plus二次开发而成后台管理系统。Laravel社区也有许多非常优秀的后台管理系统,例如Nova, 官方出品,当然是收费的,免费的有基于Livewire的Filament,还有不得不说的Laravel Admin。CatchAdmin还是采用传统的前后端分离策略,Laravel框架仅仅作为Api输出。将管理系统模块之间的耦合降到了最低限度。每个模块之间都有独立的控制器,路由,模型,数据表。在开发上尽可能将模块之间的影响降到最低,降低了开发上的难度。基于CatchAdmin可以开发CMS,CRM,OA等 等系统。也封装了很多实用的工具,提升开发体验。 V3.2.3 日志 更新 Laravel 到最新版本 11 兼容新版本,修复 Laravel 获取 Schema 字段数据 新增创建表,支持拖拽功能 修复创建表字段无法删除 删除 Laravel 新版字段类型 unsignedDecimal类型 thinkphp 8.0 版本修复 seed 类重名 thinkphp 8.0 版本修复模型错误 webman 版本仓库优化部分代...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS8编译安装MySQL8.0.19
- CentOS7,CentOS8安装Elasticsearch6.8.6