EN

Android 中的卡顿丢帧原因概述 - 应用篇

字数统计: 4.3k阅读时长: 15 min
2019/09/05
loading

技术复核 · 2026-10-13。 保留 2019 年原图、线程列表与作者观点。技术核对基线为 AOSP android-9.0.0_r1、android-16.0.0_r1;补充 API 31 帧时序与 DataStore 1.2.1 等现代入口。截图不是当前应用版本的测试结果,本次未做原设备或完整 Trace 复现。

在Android 中的卡顿丢帧原因概述 - 系统篇 这篇文章中我们列举了系统自身原因导致的手机卡顿问题, 这一篇文章我们主要列举一些由于 App 自身原因导致的卡顿问题. 各位用户在使用 App 的时候, 如果遇见卡顿现象, 先别第一时间骂手机厂商优化烂, 先想想是不是这个 App 自己的问题.

手机厂商和 App 团队会使用 Matrix 或自有方案收集线索,系统权限可增加可见信息,但数据范围仍取决于构建、采集配置和平台支持。

平均 FPS 不能单独代表流畅度。相同的 55 FPS 可以有不同的帧间隔和停顿分布;分析时需要目标节奏、实际呈现和输入响应,不能把 60→55 直接当作具体掉帧原因。本系列四篇如下:

Android App 自身导致的性能问题

下面保留 2019 年的应用案例与旧 Systrace 图。它们主要展示 View/HWUI 路径;App 名称是当时的案例归属,不代表当前版本仍有相同问题。定位时先确认关键帧,再沿线程状态和依赖追查,不能只凭一个长 slice 就把原因全部归给 App。

可结合 Systrace 系列 理解旧图;新采集使用 Perfetto。在 Android 12(API 31)起、且支持并采到对应帧数据的场景,用 FrameTimeline 关联 deadline、产帧和呈现。颜色或平均 FPS 不能代替这些证据。

1.App 主线程执行时间长

主线程中的 Input、Animation、Measure、Layout、Draw、图片解码等工作可能延迟后续帧。长区间既可能是实际计算,也可能包含 Runnable 等待调度、锁、I/O 或同步等待;应先分清墙钟时间与 Running 时间,再选择优化点。不是每帧都会完整执行所有阶段。

Measure \ Layout 耗时\超时 (或者没有调度到)

图中 measure/layout 位于较长的 traversal 中。复杂层级、重复 requestLayout 或测量逻辑是可能方向,但也要检查线程是否在 CPU 上运行;图本身不能证明“层级过深”。核对具体 View 的测量次数、调用栈和更新范围,避免一次局部变化触发不必要的整树工作。

Draw 耗时

硬件加速路径里,主线程的 Draw 通常包含记录或更新绘制命令,不等于完整 GPU 绘制。第一图可见较长 Record View#draw;第二图选区约 17.746 ms,也不能直接当作 GPU 时长或用固定 16 ms 线给所有设备定性。继续检查自定义绘制、重复记录、资源访问与线程等待。

Animation回调耗时

动画回调中的业务计算、对象分配或布局更新会增加主线程工作。图中还出现 Background concurrent copying GC,与 animation/traversal 部分重叠;不能把整个 concurrent GC 区间说成“主线程暂停”,也不能只凭共现认定动画回调是唯一原因。要看实际暂停、分配压力和调用栈。

View 初始化耗时 (PlayStore)

这张图我当时标记为 PlayStore,13.196 ms 选区里包含 animation 和多个 inflate。它提示初始化发生在关键帧内,但选区不是单次 inflate 的纯 CPU 时间。可核对是否重复创建、哪些部分可按需加载;依赖 View/UI 线程的构造工作不能简单搬到任意后台线程。

List Item 初始化耗时(WeChat)

图中 91.972 ms 区间包含 input、obtainView、inflate、layout/setupListItem 等。应检查 item 复用、绑定时的数据/图片处理和同步访问,而不是把整段都当成 inflate。这里是旧列表路径,截图没有给出 RecyclerView onBindViewHolder 的证据;新实现也应按实际组件分析。

decodeBitmap 耗时 (或者没有调度到)

图中主线程多次出现 decodeBitmap。解码区间还可能包含读取、分配与等待,要结合线程状态看。通常可在合适的后台执行器完成读取/解码,按显示尺寸控制图片大小,再在 UI 线程交付结果;同时处理页面销毁、请求取消和缓存容量,避免只是把开销移成后台竞争。

2.uploadBitmap 耗时

Bitmap 解码为像素与将像素用于 GPU 纹理是两个阶段。大图、反复修改内容或缓存淘汰可能带来纹理上传开销;不是每次绘制同一张 Bitmap 都一定重新上传,也不是所有版本都会显示同名 uploadBitmap slice。

检查上传尺寸/次数、是否每帧生成新像素,以及 RenderThread 的运行、资源分配和同步等待。优先减少不必要的像素量与重复上传,复用稳定内容;不能只增加一个没有容量限制的缓存。没有新增实测截图,此节是对原空缺解释的补充。

3.BuildDrawingCache 耗时

重复 buildDrawingCache/SW Layer 表明软件缓存绘制路径在执行,不证明业务代码每帧显式调用该 API,也不证明每次都重新分配 Bitmap。内容更新或软件 layer 本身可能触发它;先核对谁使缓存失效、尺寸是否变化。

Android 9 View.java 会复用尺寸匹配的缓存 Bitmap。旧 drawing-cache API 从 API 28 起弃用,不应作为现代通用加速方案。下方微信动态表情图也是当时的案例,不代表当前版本。

4.使用 CPU 渲染而不是 GPU 渲染

关闭 View 窗口的硬件加速会改变栅格化路径,并可能增加主线程软件绘制负担,但不是“几乎一定卡顿”。Skia 同时支持软件和 GPU 后端,不能以“走 Skia”判断没用 GPU;App 软件绘制后,系统显示合成仍可能使用 GPU/HWC。

截图未显示配置,应核对窗口/View/Canvas 的硬件加速状态和对应 API 支持。局部兼容需求不一定要关闭整个 Activity 的加速。更多边界见 Hardware Layer 详解。

5.主线程 Binder 耗时

同步 Binder 调用会让调用线程等回复,其耗时可能在对端排队、持锁工作、嵌套调用或调度等待中。图中 activityResume 区间较长,但没有展示对端调用栈或锁 owner,不能仅凭它认定“AMS 锁被后台占住”。任务/Activity 管理的服务与锁也随版本变化。

应用侧检查关键帧中是否重复做可缓存、可异步的远程查询;系统侧沿同一次 Binder 事务查接收线程和依赖。不能把框架要求在主线程执行的生命周期调用直接搬走。

6.游戏 SurfaceView 内容绘制不均匀

游戏引擎的产帧节奏与 SurfaceFlinger 的消费/呈现节奏不匹配,可能导致重复显示或延迟;是否发生 buffer 丢弃,还要看队列与呈现记录。图中 GLThread onDrawFrame 和 SurfaceView 的相位不同,红框不能独立证明“游戏算得慢”。

游戏可以主动选择低于屏幕刷新率的目标 FPS,并非必须始终满帧。应检查 CPU、GPU、提交时刻与 frame pacing。现代 OpenGL/Vulkan 游戏可参考 Android Frame Pacing(Swappy),它帮助安排提交节奏,不会自动消除超出预算的工作量。

7.WebView 性能不足

WebView 中的 JS、样式/布局、绘制、图片、网络或宿主交互都可能成为瓶颈,不能概括为 WebView 先天比原生慢。下图只显示 Chrome_IOThread 和 glide-animation 的局部活动,没有页面 profile,不能单凭它确定具体原因。

记录 WebView provider/版本,在调试构建中使用 Chrome DevTools 调试 WebView(Android 4.4 / API 19 起)查看页面主线程任务与渲染,再与系统 Trace 对齐。不要只按 Chrome 线程名称推断是哪段 JS。

8.帧率与刷新率不匹配

FPS 是内容更新频率,Hz 是显示刷新频率。固定 90 Hz 显示 60 FPS 内容时,若不能切换到兼容节奏,可能形成不均匀重复显示;60 FPS 配 120 Hz、30 FPS 配 90 Hz 则可分别均匀保持两个、三个刷新周期。低 FPS 仍会影响运动细腻程度,整数匹配也不代表输入延迟为零。

下面旧图的注释是“90 Hz 每三个 Vsync 显示一帧”,对应 30 FPS 的整数匹配示例,不是 60 FPS / 90 Hz 的不匹配证据。 应检查实际呈现间隔和显示模式,而不是仅比较两个名义数字。

9.应用性能跟不上高帧率屏幕和系统

60、90、120 Hz 的刷新间隔约为 16.67、11.11、8.33 ms。更高刷新率通常要求更短的关键路径,但主线程、RenderThread、GPU 和合成存在流水线与不同 deadline;不能把刷新间隔直接等同某一线程的全部预算,也不能说“14 ms 就一定满足 60 Hz”。

下图仍是 buildDrawingCache/SW Layer 的历史片段,没有同一 App 在 60/90 Hz 下的受控对照。应固定工作量与系统条件,比较 actual/expected frame、阶段耗时和帧数,再判断是否跟得上目标刷新率。

10.主线程 IO 操作

主线程上的数据库查询、文件读写、序列化或等待后台 I/O 都可能延迟 UI。先确认具体调用与等待点;将实际存储操作放到合适的异步执行路径,避免主线程随后同步等待结果,并合并不必要的频繁写入。

SharedPreferences.Editor.commit() 同步等待持久化结果并返回成功与否;API 9 起的 apply() 先更新内存、异步安排写盘,不返回写入成功状态。但 apply 不是“不会阻塞主线程”的保证:生命周期切换中 framework 可能等待尚未完成的写入,后续 commit 也可能等已有异步写入。Android 16 SharedPreferencesImpl 的 apply 注册 finisher,QueuedWork.waitToFinish 会处理并等待这些工作;首次读取也可能等待加载完成。

因此不能只机械地把 Commit 换成 Apply。需要错误反馈或持久化完成保证时,在后台处理 commit 及其结果;新设置存储可评估 DataStore 1.2.1 的异步接口,迁移时保留原数据语义,不要用主线程阻塞等待抵消异步收益。

11.WebView 与主线程交互

WebView 可能涉及宿主 UI、renderer、compositor 与 GPU 相关线程/进程,架构取决于 provider 和版本。第一图明确有 sandboxed_process0,后面微信图又分别展示 toolsmp 与主进程;不能把这些轨道当成同一个主线程。

同步等待、回调内重工作或消息往返可能拖慢页面,但需要把同一次请求、回调和渲染任务关联起来。截图中的调度空档不能自动归为 JS bridge 错误;宿主 UI 的绿色帧标记也不保证网页内容及时更新。应结合页面性能记录、调度和宿主调用栈检查。

12.RenderThread 耗时

RenderThread 的长 DrawFrame 区间可能包含准备树、纹理处理、驱动提交、buffer/fence 等待或调度延迟,并不等于“GPU 命令过多”或 GPU 执行时间。主线程也不是每帧都等到 GPU 完成才继续。

Android 9 DrawFrameTask 与 Android 16 DrawFrameTask 都根据 syncFrameState 的结果决定是否先 unblock UI,再继续 draw;某些条件下则延后释放。图中的 syncFrameState/prepareTree 是继续追查的线索,要确认主线程等待的具体对象及 RenderThread 当时在做什么。

13.同名 RenderThread 与同步等待

线程名可以重复。AOSP Android 9 / Android 16 的 HWUI RenderThread::getInstance 路径各维护一个进程内实例,不能仅凭多个同名线程就断言 App 创建了多个 HWUI RenderThread,或主线程在等待它们全部同步。

下图进程 PID 是 4439,后面的 ps 输出 PID 是 10300,不是已确认的同一次现场。 旧 ps 输出保留用于说明同名线程现象:应继续用 PID/TID、调用栈、创建来源和等待关系识别线程,而不是按名称直接归因。

原采集命令(历史 PID,未在本次执行;grep 是子串匹配):

1
adb shell ps -AT | grep 10300 | grep RenderThread
1
2
3
4
5
6
7
8
9
u0_a170      10300 16228  6709 2693260 305172 SyS_epoll_wait      0 S RenderThread
u0_a170 10300 17394 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17395 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17396 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17397 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17399 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17400 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17401 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17402 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread

这些 S 状态、epoll/futex 等待点只是当时的快照,不能证明线程死锁、持续繁忙或 UI 的等待原因。

总结

下面是我在 2019 年的一些观察,涉及当时的应用版本和市场环境。原文写作时的 Android Q 是 Android 10;Android 原生系统持续演进, 每个版本都会解决非常多的性能问题, 同时也会引进一些问题 ; 到了手机厂商这里, 由于硬件差异和软件定制, 会在系统中加入大量的自己的代码, 这无疑也会影响系统的性能 . 同样由于 Android 的开放, App 的质量和行为也影响着整机的用户体验.

本篇主要列出了 App 自身的实现问题导致的流畅性问题, Android App 最大的问题就是质量良莠不齐, 不同于 App Store 这样的强力管理市场, Android App 不仅可以在 Google Play 上面进行安装, 也可以在其他的软件市场上面安装, 甚至可以下载安装包自行安装, 可以说上架的门槛非常低, 那么质量就只能由 App 开发者自己来把握了.

许多大厂的 App 质量自然不必多说, 他们对性能和用户体验都是非常关注的, 但也会有需求和功能过多导致的性能问题, 比如微信就非常占内存 ; 新版本的 QQ 要比之前版本的使用起来流畅性差好多 . 中小厂的 App 就更不用说了. 再加上 Android 平台的开放性, 需要 App 玩起来黑科技, 什么保活 \ 相互唤醒 \ 热更新 \ 跑后台任务等 . 站在 App 开发者的角度来说这无可厚非, 但是系统开发者则希望系统能在用户使用的时候, 前后台 App 都能有正常的行为, 来保证前台 App 的用户体验 . 也希望 App 开发者能重视自己 App 的性能体验, 给用户一个好印象.

系统这边发现 App 自身的性能问题, 且在其他厂商的手机上也是一样的表现的时候, 通常会与 App 开发者进行联系, 沟通一起解决 .

大家可以看看这个问题 : 当手机厂商说安卓手机性能优化的时候,他们到底在做什么

这也是流畅性的一个系列文章中的一篇, 可以点击下面的链接查看本系列的其他文章.

本文知乎地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 中的卡顿丢帧原因概述 - 应用篇

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. Android App 自身导致的性能问题
    1. 1.1. 1.App 主线程执行时间长
      1. 1.1.1. Measure \ Layout 耗时\超时 (或者没有调度到)
      2. 1.1.2. Draw 耗时
      3. 1.1.3. Animation回调耗时
      4. 1.1.4. View 初始化耗时 (PlayStore)
      5. 1.1.5. List Item 初始化耗时(WeChat)
      6. 1.1.6. decodeBitmap 耗时 (或者没有调度到)
    2. 1.2. 2.uploadBitmap 耗时
    3. 1.3. 3.BuildDrawingCache 耗时
    4. 1.4. 4.使用 CPU 渲染而不是 GPU 渲染
    5. 1.5. 5.主线程 Binder 耗时
    6. 1.6. 6.游戏 SurfaceView 内容绘制不均匀
    7. 1.7. 7.WebView 性能不足
    8. 1.8. 8.帧率与刷新率不匹配
    9. 1.9. 9.应用性能跟不上高帧率屏幕和系统
    10. 1.10. 10.主线程 IO 操作
    11. 1.11. 11.WebView 与主线程交互
    12. 1.12. 12.RenderThread 耗时
    13. 1.13. 13.同名 RenderThread 与同步等待
  2. 2. 总结
  3. 3. 本文知乎地址
  4. 4. 关于我 && 博客