Android main thread主线程Choreographer doFrame发生FullSuspendCheck
1. FullSuspendCheck 是什么
ART 的 Java/Kotlin 线程并不是任意时刻都能立刻安全暂停。通常线程需要执行到某些 ART 已知安全的位置,称为:
safepoint
suspend check point
在这些位置,ART 会检查当前线程是否被要求暂停。通常情况下,这类检查很快,类似:
是否有 suspend request?
没有。
继续执行。
但如果发现当前线程存在 suspend request,就会进入较重的路径:
FullSuspendCheck
可大致理解为:
ART 发现当前线程需要响应挂起请求。
当前线程进入可被安全检查、暂停或协调 runtime 操作的流程。
因此,FullSuspendCheck 不常见且耗时长,往往意味着当时不是普通代码执行,而是 ART runtime 正在进行某种需要线程配合的全局或半全局操作。
2. 为什么会发生在 Choreographer#doFrame 中
因为主线程正在运行 doFrame,而 ART 的 suspend check 可以插入或出现在 Java/Kotlin 执行过程中的多个安全点,例如:
方法调用边界
循环回边
滑动、View traversal、绘制等逻辑
↓
运行到 ART safepoint
↓
ART 发现存在 pending suspend request
↓
进入 FullSuspendCheck
↓
主线程被暂停/等待/协助 runtime 操作
↓
返回后继续执行 doFrame
所以它出现在 doFrame 内,只是说明:
主线程刚好在该帧执行期间响应了 ART 的挂起请求。
并不表示:
Choreographer 或大图滑动代码直接调用了 FullSuspendCheck。
3. FullSuspendCheck 常见触发原因
从常见程度和关联度看,优先排查以下几类。
原因一:GC 相关的线程暂停
这是最需要优先确认的一类。
ART 在执行某些 GC 阶段时,需要让 Java 线程进入 safepoint,或者等待线程到达可安全扫描/处理的状态。
典型链路:
大图左右滑动期间产生较多对象、Bitmap、Drawable、临时集合或图片解码相关对象
↓
Java heap / native heap 压力上升
↓
ART 发起 GC
↓
GC 某阶段要求 mutator 线程响应 suspend request
↓
图库主线程执行到 safepoint
↓
进入 FullSuspendCheck
↓
主线程暂停或等待 GC 相关操作
↓
doFrame 被拉长
注意:
并发 GC 不代表对 UI Thread 完全没有影响。
即使 GC 的主体工作在后台线程上进行,仍可能有需要应用线程配合的阶段,或者存在短暂停顿。
应在同一时间窗口检查:
HeapTaskDaemon
GC Thread
Concurrent GC
Marking
Sweep
Compact
Young GC
Explicit GC
WaitForGcToComplete
不同 Android 版本的 trace 名称会有区别。
原因二:线程创建、线程退出、Attach/Detach 等 ART ThreadList 操作
ART 维护一份 runtime 线程列表。
当线程创建、销毁,或 native 线程执行 JNI attach/detach 时,可能涉及 thread list、线程状态切换及相关同步。
如果应用频繁创建线程,例如:
临时图片解码线程
协程调度器工作线程扩容
自建线程池临时建线程
JNI/native 图片处理线程 attach/detach
媒体/相机/相关 native 工作线程
就可能增加 ART runtime 线程管理操作。
这类路径不一定每次都导致全局暂停,但如果 trace 同期出现:
Thread.start
ThreadList
AttachCurrentThread
DetachCurrentThread
CreateNativeThread
DestroyJavaVM
就需要重点关联。典型链路:
某个调试、采样或监控组件请求线程栈
↓
ART 发起线程 suspend / checkpoint 操作
↓
主线程在 safepoint 响应
↓
出现 FullSuspendCheck
如果这是在:
debug 包
开启 Android Studio Profiler
开启 method trace
开启 heap tracking
接入性能监控或崩溃监控实验功能
环境下复现,需要优先排除这类因素。
原因四:JIT、类加载、运行时内部。
4. 为什么会出现 FullSuspendCheck
可能有几种情况:
情况一:同一个 runtime 操作的多阶段协作
例如某次 GC 或 ART runtime 操作不是一次主线程暂停就完全结束,而是在不同阶段需要 mutator 线程再次配合。
主线程第一次到达 safepoint
↓
第一次 FullSuspendCheck
↓
恢复执行
↓
GC/runtime 操作进入下一阶段
↓
主线程再次到达 safepoint
↓
第二次 FullSuspendCheck
情况二:短时间内连续出现两次独立 suspend 请求
例如:
一次 GC 相关请求
↓
主线程恢复
↓
随后又发生线程管理、调试采样或另一轮 GC 相关请求
↓
主线程再次进入 FullSuspendCheck
情况三:主线程第一次没有立即完成所需协调,后续再次检查
这需要看具体 ART 版本及 trace 周边事件确认。
但总体含义仍然是:
主线程在同一帧中多次被 runtime suspend 存在 pending suspend/checkpoint 请求
↓
主线程第一次运行到 safepoint
↓
进入 FullSuspendCheck,耗时约 20ms 的一部分
↓
主线程恢复后继续执行 doFrame
↓
再次运行到 safepoint
↓
再次进入 FullSuspendCheck
↓
doFrame 总耗时被拉长至 80ms+
↓
90Hz 下错过约 7 帧
↓
大图左右滑动明显卡顿
90Hz 帧预算:
1000 / 90 ≈ 11.11ms
因此:
80ms / 11.11ms ≈ 7.2 帧
而单次 20ms+ 的 FullSuspendCheck 本身已经超过一帧预算。
6. 在 Perfetto/Trace 中如何继续定位
以 FullSuspendCheck 的起止时间为中心,前后各扩 50ms~200ms 看完整时间线。
7.1 优先找 ART / GC 轨道
搜索这些关键词:
GC
HeapTaskDaemon
HeapTask
Concurrent
MarkSweep
Sweep
Compaction
Marking
WaitForGcToComplete
SuspendAll
ResumeAll
Checkpoint
ThreadList
如果它们与 FullSuspendCheck 重叠,尤其是有:
SuspendAll
GC
HeapTaskDaemon
那么 GC 相关性很高。
7.2 看进程中的线程状态
重点看这些线程在该时段是否活跃:
HeapTaskDaemon
Jit thread pool
Signal Catcher
ReferenceQueueDaemon
↓
ART GC 或系统内存回收
↓
主线程 FullSuspendCheck
↓
滑动卡顿
7.4 排除调试和监控影响
确认复现环境是否有:
Android Studio Profiler
CPU method trace
heap profiling
debugger attached
StrictMode
自研 ANR watchdog
自研线程栈采样
Crash/性能 SDK 的高频堆栈采集
建议对比:
debug 包 + profiler 开启
debug 包 + profiler 关闭
release 包
如果只在 profiler/debug 环境下频繁出现,优先考虑调试采样造成的 runtime 挂起干扰。
8. 最终重点
最需要确认的是:
这两次 FullSuspendCheck 同期,是否存在 GC / SuspendAll / HeapTaskDaemon 活动?
如果有,优先沿着:
Bitmap/对象分配
↓
Java/native 内存压力
↓
GC/runtime suspend
↓
FullSuspendCheck
↓
UI Thread doFrame 卡顿
去定位。
如果没有 GC 痕迹,则下一优先级是:
是否发生了协程/线程创建或 native Attach/Detach,
以及是否有 profiler、ANR/Crash 栈采集等线程挂起操作。