Android 卡顿概述

说到 Android 卡顿,其实是个老生常谈的话题。我自己做性能优化这些年,见过太多因为卡顿被用户骂到下架的 App。说白了,卡顿就是用户操作界面时,画面不跟手、掉帧、或者直接冻住几秒。这种体验,用户往往只会给一次机会——第一次卡顿,就可能卸载。

卡顿的定义与影响

我们通常用 帧率(FPS) 来量化卡顿。Android 系统以 60Hz 刷新率为标准,意味着每 16.6ms 要渲染一帧。一旦某一帧超过这个时间,就产生 丢帧(Jank)。连续丢帧,用户就感觉“卡”。

卡顿的核心指标:

  • FPS:低于 45 一般就能感知到掉帧。
  • 帧时间:超过 16.6ms 算一次小微卡,超过 100ms 就是严重卡顿。
  • 掉帧率:单位时间内掉帧的占比,超过 5% 就需要警惕。

影响有多严重?我记得之前做一个短视频 App,首页滑动列表在低端机上经常卡到 20FPS。用户反馈里全是“滑动像PPT”、“点开视频要等3秒”。后来我们花了两个迭代专门做流畅性优化,日活才慢慢回升。卡顿不仅影响用户体验,还直接拉低留存和商业转化。

还有一个隐藏影响:耗电。卡顿往往伴随着 CPU/GPU 的高频负载,反而比流畅时更费电。嗯,这里要注意,很多人以为“卡顿就是慢”,其实它背后还藏着电池、发热等问题。

常见卡顿原因

搞性能优化,第一步得知道敌人长什么样。我把常见原因总结成三类:UI 阻塞、GC 频繁、IO 拖拽。下面这张图可以帮你快速建立整体认知:

Android 卡顿原因与优化目标框架 App 卡顿 UI 阻塞 GC 频繁 IO 拖拽 过度绘制 布局嵌套深 内存分配过大 对象频繁创建 主线程读写 优化目标 帧率稳定 ≥ 55 FPS | 帧时间 ≤ 16ms | 响应及时 ≤ 100ms

你看,这三类原因分别对应不同的系统资源瓶颈。咱们一个个掰开聊。

1. UI 阻塞 —— 主线程的“跑不动”

Android 的 UI 更新都在主线程(也叫 UI 线程)。一旦主线程在执行耗时操作——比如复杂的自定义 View 绘制、深层布局测量、或者直接在主线程做网络请求——那么下一帧就无法按时触发。这就是最直观的“点一下等半天”。

我曾经在一个相册 App 里遇到过首页列表卡顿。排查后发现,每个 item 的 ImageView 都用了超大分辨率原图,而且没有做缩略图。Result: 每滑一帧都要解码一张几 MB 的图片,16ms 根本搞不定。后来改用 Glide 缩略图加缓存,帧率直接从 25 飙到 55。

避坑指南:

我曾经在自定义 View 里直接用 new Paint() 每次 onDraw 都创建新对象,结果导致频繁 GC 和绘制卡顿。后来改成全局复用一个 Paint 实例,问题解决了。记住:onDraw 里不要做任何对象分配,不要做 IO,不要做复杂计算。

2. GC 频繁 —— 内存的“大扫除”

Java 虚拟机的垃圾回收(GC)会暂停所有线程(STW,Stop The World)。当 GC 频率高、耗时久时,主线程就被“卡死”几十甚至几百毫秒。用户感觉就像突然顿一下,然后恢复。

常见场景:滑动列表时,Adapter 里频繁创建新对象(比如每 getView 都 new 一堆临时变量)。这些对象升到老年代后,触发 GC 的时间就越长。我统计过一个项目,GC 耗时占了总帧时间的 15%,优化后降到 3% 以下,体验提升非常明显。

GC 卡顿典型特征:

  • 帧时间曲线出现“尖刺”,每几秒一次峰值。
  • Logcat 里看到 GC_FOR_ALLOC 或 GC_CONCURRENT 频繁出现。
  • 使用 Profile 工具能看到内存分配速率居高不下。

3. IO 拖拽 —— 磁盘的“慢动作”

Android 设备的磁盘 I/O 比内存慢好几个数量级。如果在主线程里读写文件、数据库查询、或者 SharedPreferences 的 commit,都会阻塞 UI。尤其是低端机,I/O 速度更慢。

我记得一个用户反馈:进入聊天记录页面要等 2 秒才能看到列表。一看代码——直接在主线程执行了全表查询,数据量 5000 条。后来加了分页加载和异步查询,首屏响应时间降至 200ms。

注意:

不要觉得“SharedPreferences 很快”。commit 是同步写入磁盘的,频繁调用会导致主线程卡顿。建议用 apply 或者换成 MMKV。另外,Room 数据库的查询一定要在后台线程做,主线程跑查询就是个定时炸弹。

优化目标 —— 我们到底要追求什么?

说了这么多原因,该聊聊目标了。我个人习惯把优化目标拆成三个层次:

维度 目标值 衡量工具
帧率稳定 滑动场景 ≥ 55 FPS,静态页面 ≥ 50 FPS GPU Profile、Systrace、Perfetto
帧时间 每帧绘制耗时 ≤ 16ms(60Hz) FrameTimeline、Logcat 的 CHORE 输出
响应及时 用户点击到界面反馈 ≤ 100ms TouchLatency、手动秒表

你想想看,这三个目标其实互相影响。帧时间超了,帧率自然掉;帧率掉了,用户感觉响应也慢了。所以优化时不要单看一个指标,得综合分析。

还有一点:不要追求不卡。实际项目中,低端机偶尔掉一帧很正常。我们定目标时通常以“用户无感知”为标准——掉帧率低于 2%,且没有连续掉帧超过 3 帧,就合格了。

个人经验:

我曾在一个日活千万的项目里做过一次“零卡顿”改造。花了两周时间把所有主线程 IO 全异步化、优化所有列表的 ViewHolder、用对象池代替 new,最终上线后崩溃率反而上升了——因为异步线程数过多导致内存泄漏。后来我学乖了:性能优化不能过度,要找到核心瓶颈逐步解决。

总结一下这一章:卡顿的根源无非是主线程被阻塞,原因分三类 —— UI 阻塞(绘制、布局)、GC 频繁(内存分配)、IO 拖拽(磁盘读写)。优化的目标就是让主线程每 16ms 能安稳跑完一帧。后面我们会逐层深入,从工具定位到代码调优,一步步把手伸进去。