Android 卡顿概述
说到 Android 卡顿,其实是个老生常谈的话题。我自己做性能优化这些年,见过太多因为卡顿被用户骂到下架的 App。说白了,卡顿就是用户操作界面时,画面不跟手、掉帧、或者直接冻住几秒。这种体验,用户往往只会给一次机会——第一次卡顿,就可能卸载。
卡顿的定义与影响
我们通常用 帧率(FPS) 来量化卡顿。Android 系统以 60Hz 刷新率为标准,意味着每 16.6ms 要渲染一帧。一旦某一帧超过这个时间,就产生 丢帧(Jank)。连续丢帧,用户就感觉“卡”。
卡顿的核心指标:
- FPS:低于 45 一般就能感知到掉帧。
- 帧时间:超过 16.6ms 算一次小微卡,超过 100ms 就是严重卡顿。
- 掉帧率:单位时间内掉帧的占比,超过 5% 就需要警惕。
影响有多严重?我记得之前做一个短视频 App,首页滑动列表在低端机上经常卡到 20FPS。用户反馈里全是“滑动像PPT”、“点开视频要等3秒”。后来我们花了两个迭代专门做流畅性优化,日活才慢慢回升。卡顿不仅影响用户体验,还直接拉低留存和商业转化。
还有一个隐藏影响:耗电。卡顿往往伴随着 CPU/GPU 的高频负载,反而比流畅时更费电。嗯,这里要注意,很多人以为“卡顿就是慢”,其实它背后还藏着电池、发热等问题。
常见卡顿原因
搞性能优化,第一步得知道敌人长什么样。我把常见原因总结成三类:UI 阻塞、GC 频繁、IO 拖拽。下面这张图可以帮你快速建立整体认知:
你看,这三类原因分别对应不同的系统资源瓶颈。咱们一个个掰开聊。
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 能安稳跑完一帧。后面我们会逐层深入,从工具定位到代码调优,一步步把手伸进去。