前言
线上突然收到「滑动卡顿率飙升」的告警,用户反馈「点个按钮都要等半天」。这种问题不像崩溃那样有明确堆栈,往往需要从布局层级、主线程耗时、内存抖动、过度绘制等多个维度全链路排查。
本文记录一次真实的线上卡顿治理过程,展示如何系统性地定位并解决性能瓶颈。
一、问题现场:卡顿率从 5% 飙到 25%
1.1 告警信息
diff
[性能监控] 卡顿率异常
- 时间段:2024-03-15 14:00 - 16:00
- 受影响版本:3.2.0
- 卡顿率:25.3%(基线 5%)
- 主要场景:首页商品列表滑动
1.2 用户反馈
- 「列表滑动时一卡一卡的」
- 「点击商品详情后白屏 2 秒才出来」
- 「返回列表时又卡一下」
二、排查策略:四个维度逐一击破
2.1 维度一:布局层级过深
检查手段
Android Studio → Layout Inspector → 选中首页 Activity
发现问题:
首页商品列表 item 布局层级:13 层
├─ ConstraintLayout(根布局)
│ └─ CardView(卡片容器)
│ └─ LinearLayout(内容容器)
│ └─ RelativeLayout(左侧图片区)
│ └─ FrameLayout(图片蒙层)
│ └─ ImageView
│ └─ LinearLayout(右侧信息区)
│ └─ ...(继续嵌套 6 层)
治理方案
Before(13层嵌套)
xml
<androidx.cardview.widget.CardView>
<LinearLayout>
<RelativeLayout>
<FrameLayout>
<ImageView />
</FrameLayout>
</RelativeLayout>
<LinearLayout>
<!-- 右侧信息区 -->
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
After(4层扁平化)
xml
<androidx.cardview.widget.CardView>
<androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/productImage"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/productName"
app:layout_constraintStart_toEndOf="@id/productImage"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/productPrice"
app:layout_constraintStart_toEndOf="@id/productImage"
app:layout_constraintTop_toBottomOf="@id/productName" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
效果: 层级从 13 层降为 4 层,布局耗时从 18ms 降为 6ms。
2.2 维度二:主线程同步加载
检查手段
使用 Systrace 录制滑动场景:
bash
python systrace.py -t 10 -o trace.html sched gfx view wm am app
在 Chrome 中打开 trace.html,发现主线程在 onBindViewHolder 中执行了:
- 同步解码大图(耗时 120ms)
- 同步读取本地数据库(耗时 80ms)
- 同步网络请求(耗时 200ms)
治理方案
Before(主线程全干)
kotlin
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val product = productList[position]
// 主线程解码图片
val bitmap = BitmapFactory.decodeFile(product.imagePath)
holder.image.setImageBitmap(bitmap)
// 主线程查数据库
val price = database.queryPrice(product.id)
holder.price.text = "¥$price"
// 主线程网络请求
val stock = api.getStock(product.id)
holder.stock.text = "库存: $stock"
}
After(异步 + 缓存)
kotlin
// 1. 图片异步加载 + LruCache
private val imageCache = LruCache<String, Bitmap>(10 * 1024 * 1024)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val product = productList[position]
// 异步解码图片
val cached = imageCache.get(product.imagePath)
if (cached != null) {
holder.image.setImageBitmap(cached)
} else {
lifecycleScope.launch(Dispatchers.IO) {
val bitmap = decodeSampledBitmap(product.imagePath, 200, 200)
imageCache.put(product.imagePath, bitmap)
withContext(Dispatchers.Main) {
holder.image.setImageBitmap(bitmap)
}
}
}
// 2. 数据库查询迁移到 ViewModel + Room
holder.price.text = "¥${product.cachedPrice}" // 提前加载到内存
// 3. 网络请求改为预加载 + 本地缓存
holder.stock.text = stockCache[product.id] ?: "加载中"
}
效果: 主线程 onBindViewHolder 耗时从 400ms 降为 8ms。
2.3 维度三:过度绘制
检查手段
开发者选项 → GPU 过度绘制调试 → 首页大片红色(4x overdraw)
定位问题:
- 列表背景 + Item 背景 + CardView 背景 = 3 层白色重叠
- 商品图片设置了不透明白色背景
- RecyclerView 设置了全屏背景图
治理方案
xml
<!-- Before -->
<RecyclerView
android:background="@drawable/bg_gradient" /> <!-- 第1层 -->
<androidx.cardview.widget.CardView
android:background="@color/white"> <!-- 第2层 -->
<LinearLayout
android:background="@color/white"> <!-- 第3层 -->
<ImageView
android:background="@color/white" /> <!-- 第4层 -->
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- After -->
<RecyclerView
android:background="@drawable/bg_gradient" /> <!-- 只保留最外层 -->
<androidx.cardview.widget.CardView> <!-- 移除背景 -->
<ConstraintLayout> <!-- 移除背景 -->
<ImageView /> <!-- 移除背景 -->
</ConstraintLayout>
</androidx.cardview.widget.CardView>
效果: 过度绘制从红色(4x)降为蓝色(1x),滑动帧率从 42fps 提升到 58fps。
2.4 维度四:内存抖动
检查手段
Memory Profiler → Record allocations → 滑动列表 → Stop
发现每次滑动产生 200+ 个临时对象:
diff
频繁创建的对象:
- String(拼接商品信息)
- ArrayList(每次 onBindViewHolder 都 new)
- Bitmap(重复解码)
治理方案
kotlin
// Before(每次都创建)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val product = productList[position]
val info = ArrayList<String>() // 每次都 new
info.add("商品名称:${product.name}")
info.add("价格:¥${product.price}")
holder.info.text = info.joinToString("
")
}
// After(复用 + StringBuilder)
private val infoBuilder = StringBuilder()
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val product = productList[position]
infoBuilder.clear()
infoBuilder.append("商品名称:").append(product.name)
infoBuilder.append("
价格:¥").append(product.price)
holder.info.text = infoBuilder.toString()
}
效果: 滑动过程中 GC 日志从每秒 8 次降为每 15 秒 1 次。
三、全链路治理效果
| 维度 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 布局层级 | 13层 | 4层 | 布局耗时 -67% |
| 主线程耗时 | 400ms | 8ms | -98% |
| 过度绘制 | 4x | 1x | 帧率 +38% |
| GC频率 | 8次/秒 | 1次/15秒 | -94% |
| 卡顿率 | 25.3% | 3.8% | -85% |
四、持续监控机制
4.1 自动化卡顿监控
kotlin
class PerformanceMonitor : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
val diff = (System.nanoTime() - frameTimeNanos) / 1_000_000
if (diff > 16) { // 超过一帧时间
reportLag(diff)
}
Choreographer.getInstance().postFrameCallback(this)
}
}
4.2 线上分层告警
yaml
告警规则:
- 卡顿率 > 10%: P2 告警
- 卡顿率 > 20%: P1 告警
- 主线程耗时 > 100ms: 自动采样上报堆栈
五、总结
线上卡顿治理不是单点优化,而是布局 + 主线程 + 绘制 + 内存的全链路作战:
- 布局层级:ConstraintLayout 扁平化,减少嵌套
- 主线程负载:异步加载 + 数据预取 + 缓存
- 过度绘制:移除冗余背景,减少图层叠加
- 内存抖动:对象复用 + StringBuilder,减少 GC
优化后卡顿率从 25% 降到 4%,用户五星好评率提升 12 个百分点。性能优化的本质是系统性思维 + 工具链 + 持续监控。