卡顿 — 是用户对移动应用运行缓慢且不稳定情况的描述:时而正常响应,时而突然卡住几秒钟。在技术语境中,“卡顿”是指由频繁的GC暂停、同步操作阻塞主线程以及非最优数据结构引起的延迟和微卡顿的组合。根据 Android Performance Benchmarking Guide,将响应时间从 300 ms 降低到 100 ms 可将用户留存率提高 25%。卡顿的诊断需要结合 CPU 和 Memory 性能分析以及垃圾回收频率分析。
要点概述
卡顿 — 用户用来描述主观感觉应用运行缓慢的非正式术语。与表现为持续延迟的延迟不同,卡顿是不规则的冻结:应用可以完美运行几秒钟,然后“思考”1–3 秒。
从性能分析的角度来看,卡顿 表现为一系列丢帧 (jank),峰值延迟超过 100 ms。在 FPS 图表上,这看起来像急剧下降:60 → 20 → 55 → 10 帧/秒。与 FPS 均匀较低的延迟不同,卡顿具有明显的变异性。
当应用 卡顿 时,用户无法理解减速的逻辑:屏幕可以平滑滚动,然后突然停止一秒钟。这会引起挫败感并降低对应用的信任。根据 Google 的数据,53% 的用户会在加载时间超过 3 秒时离开网站或应用。
卡顿的不规则特性表明问题是由事件因素引起的,而不是持续过载。让我们来看典型的场景。
在 Android 的 ART 环境中,垃圾回收会停止应用的所有线程。如果代码中创建了大量临时对象——例如,每次调用 onBindViewHolder 通过连接创建新的 String——GC 会更频繁地启动。暂停时间取决于堆大小和对象代际,可能持续 5–50 ms。用户会感觉到突然的“思考”。
Room 在 Android 和 Core Data 在 iOS 上支持异步查询,但开发人员为了简单起见经常调用 getValue() 或通过 runBlocking 执行查询。对 10,000 行表进行连接的繁重 SELECT 可能需要 200–500 ms,在此期间完全阻塞 UI。
加载相机图像 (12 Mp, 4000x3000 px) 而不缩放需要长达 200 ms 来解码为 Bitmap。如果图像是异步加载但没有限制的线程池,同时启动 5–6 个解码可能会导致 CPU 过载,引起迁移性减速。
不规则减速的诊断比恒定延迟的诊断更困难,因为问题可能不会在每次启动时重现。需要长时间收集统计数据。
Android Studio Memory Profiler 不仅显示内存使用情况,还显示 GC 事件:频率、类型(Concurrent、Full)、持续时间。如果在静止状态下 GC 每 5 秒发生超过 1 次——这是过度分配的迹象。在卡顿时记录堆转储可以看到哪些对象占用了内存。
在 iOS 上使用 Instruments 中的 Allocations 模板来跟踪对象的创建和释放。启用代数 (Generations)——它们允许在操作之间拍摄堆快照并查看哪些对象留在内存中。未释放的持久对象——内存积累和后续暂停的来源。
JankStats — 实时收集丢帧指标的 Android 库。它将每个 jank 与当前场景(例如“列表滚动”、“打开屏幕”)关联起来,从而可以了解卡顿发生在哪个具体操作上。
在 Android 上集成 JankStats 跟踪卡顿的示例:
class MainActivity : AppCompatActivity() {
private lateinit var jankStats: JankStats
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jankStats = JankStats.create(this.window.decorView) { frameData ->
if (frameData.isJank()) {
Log.w("Jank", "Duration=${frameData.durationMs}ms")
}
}
}
}
消除卡顿需要针对每个原因进行有针对性的工作。没有通用解决方案——需要分析具体的性能概况。
如果列表包含 1000+ 个元素并且全部一次性加载——这保证会卡顿。Android 上的 Paging 3 和 iOS 上的 NSFetchedResultsController 在滚动时分块加载数据。用户只看到前 10–20 个元素,其余的在后台加载。
Room 允许通过 Android Studio 中的 Inspection Tool 进行查询性能分析:可以看到执行时间、返回的行数和查询计划。在 WHERE 和 ORDER BY 列上添加索引可以将查询时间从 300 ms 减少到 5 ms。在 iOS 上,类似的检查由 Instruments 中的 Core Data Profiler 执行。
后台同步、文件上传、数据处理——所有这些都应该通过 WorkManager(Android)或 Background Tasks(iOS)执行。如果同步在 UI 线程中启动,应用将在执行期间卡顿。WorkManager 保证在后台线程中执行,同时考虑电池和网络状态。
在 Android 上通过 WorkManager 进行后台同步的示例:
class SyncWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
Log.d("Sync", "在后台线程中同步数据")
syncData()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
在编码阶段,通过遵循高效使用内存和线程的原则,可以预防卡顿。
Baseline Profiles — Android 预先 (AOT) 编译而不是 JIT 编译的类和方法列表。没有配置文件,每个新屏幕在第一次打开时都会被编译,导致 100–500 ms 的延迟。为关键屏幕准备 Baseline Profile 并通过 baseline-profile-gradle-plugin 在 Gradle 中启用生成。
Hot path — 每帧执行的代码:onBindViewHolder、draw、layoutSubviews。避免在这些方法中创建对象:使用对象池、使用 StringBuilder 代替连接、缓存格式化字符串和格式化器。每次额外的分配都会使下一次 GC 更近。
在 CI 流水线中添加运行 Macrobenchmark,使用列表滚动和屏幕打开的场景。设置阈值:第 99 百分位帧时间不应超过 16 ms。如果超过阈值——构建将被拒绝,直到优化完成。
常见问题
延迟 — 是恒定的延迟(例如,每次点击 200 ms)。卡顿 — 不规则的:应用正常运行,然后突然变慢 1–3 秒,然后再次恢复正常。原因 — 像 GC 暂停或数据库同步查询这样的事件因素。
使用 Android Studio 中的 Memory Profiler:Memory 选项卡显示带有持续时间的 GC 事件。对于生产监控,连接带有自定义跟踪的 Firebase Performance Monitoring。在 iOS 上启用 Malloc Debug 并在 Instruments 中标记分配代际。
间接地——是的。如果服务器响应有延迟,而 UI 同步 等待它,应用会冻结。如果请求是异步的,但响应处理在 UI 线程中完成——这也会导致卡顿。解决方案——使用协程和进度指示器进行异步处理。
如果使用不当,KMP 可能会为互操作性生成过多的包装器对象。在 iOS 上,这会增加分配频率,从而增加 ARC 暂停。使用 @ObjCName,优化 expect/actual,避免从 UI 热路径频繁调用共享代码。
通过 android:largeHeap="true" 增加堆会延迟 GC,但不会消除分配的原因。当 GC 最终启动时,暂停 会更长,因为需要遍历更多对象。解决方案——减少分配数量,而不是扩大堆。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。