一、卡顿的本质:为什么UI会卡?

做UI开发这么多年,我见过太多人一遇到卡顿就慌了神。其实说白了,卡顿没那么神秘。今天我就带你彻底搞懂它。

1.1 主线程阻塞的真相

先问个问题:你的App界面为什么能流畅滑动?

答案很简单——主线程在不停地干活。它负责处理触摸事件、执行布局、绘制界面。一旦它忙不过来,卡顿就来了。

我举个例子。假设主线程是个餐厅服务员,他需要同时做三件事:

  • 接单(处理触摸事件)
  • 上菜(执行布局)
  • 摆盘(绘制界面)

如果这时候后厨(子线程)突然喊他去做报表……嗯,客人就得等着了。这就是主线程阻塞。

核心结论:主线程只做UI相关的事。任何耗时操作——网络请求、大文件读写、复杂计算——都不该出现在主线程上。

我在项目中遇到过最典型的案例:一个列表页,每次滑动都卡成PPT。查了半天,发现是列表项的onBindViewHolder里做了图片压缩。压缩操作放在主线程,不卡才怪。

1.2 帧率与刷新率的关系

你肯定听过60fps、120fps这些词。它们到底什么意思?

帧率(FPS),就是每秒能绘制多少帧画面。刷新率(Hz),是屏幕每秒刷新多少次。

这两者必须配合好。你想想看:

  • 如果帧率是60,刷新率是60:完美,一帧对一帧
  • 如果帧率是30,刷新率是60:屏幕刷新两次,画面才更新一次——你就能感觉到卡顿
  • 如果帧率是120,刷新率是60:多出来的帧就浪费了,这叫「帧撕裂」
场景 帧率 刷新率 体验
理想状态 60 FPS 60 Hz 流畅
轻度卡顿 30-45 FPS 60 Hz 能感觉到掉帧
严重卡顿 < 30 FPS 60 Hz 明显卡顿,操作延迟
ANR 0 FPS 60 Hz 应用无响应

个人习惯:我一般用Choreographer来监控帧率。它能精确告诉你每一帧花了多少时间。超过16ms(60fps的阈值)就要警惕了。

1.3 卡顿的两种类型:掉帧与ANR

卡顿其实分两种,很多人混为一谈。我帮你理清楚。

掉帧(Jank)

掉帧就是画面不连贯。比如你滑动列表,突然跳了一下。原因很简单:某一帧没在规定时间内完成。

为什么会这样?

  • 布局嵌套太深,measure/layout耗时过长
  • 绘制时做了大量计算,比如自定义View里的onDraw太复杂
  • 频繁触发GC,导致主线程被暂停

我记得有一次,一个同事写的自定义View,在onDraw里用了Paint.measureText。每次绘制都重新测量,掉帧掉到怀疑人生。改成缓存测量结果后,瞬间流畅了。

ANR(Application Not Responding)

ANR比掉帧严重得多。掉帧只是画面不流畅,ANR是应用直接「死」了。

Android系统有个机制:如果主线程超过5秒没处理完输入事件,或者广播接收器超过10秒没返回,系统就会弹ANR对话框。

避坑指南:我曾经在BroadcastReceiver里直接做了网络请求。结果用户手机网络慢,10秒没返回,ANR弹出来了。后来我改成用IntentService处理,再也没出过问题。

掉帧和ANR的区别,我用一张图来说明:

卡顿类型对比 掉帧(Jank) 特征: • 画面不连贯,有跳跃感 • 帧率低于屏幕刷新率 • 应用仍可操作 原因: • 布局嵌套过深 • 绘制计算复杂 • GC频繁触发 ANR 特征: • 应用完全无响应 • 弹出系统ANR对话框 • 用户只能等待或关闭 原因: • 主线程耗时操作 > 5秒 • BroadcastReceiver > 10秒 • 死锁或无限循环 掉帧是「慢」,ANR是「死」——两者严重程度完全不同

记住这个原则:掉帧可以忍,ANR不能忍。掉帧影响体验,ANR直接导致用户流失。我见过太多App因为ANR频繁被卸载。

嗯,到这里你应该对卡顿的本质有了清晰认识。主线程阻塞是万恶之源,帧率与刷新率的不匹配是表象,掉帧和ANR是两种不同严重程度的后果。下一章我会讲如何用工具精准定位卡顿——到时候你就能亲手抓出那些「凶手」了。

一个小建议:从现在开始,养成习惯——每次写代码前先问自己:「这段代码会不会阻塞主线程?」如果答案是「会」,那就换个方式做。

可可精品资料库