开发中的卡顿 — 是什么、原因以及优化方法

作者: IT Sectr 发布日期: 2026-07-28 阅读时间: 8 分钟

卡顿 — 是用户对移动应用运行缓慢且不稳定情况的描述:时而正常响应,时而突然卡住几秒钟。在技术语境中,“卡顿”是指由频繁的GC暂停、同步操作阻塞主线程以及非最优数据结构引起的延迟和微卡顿的组合。根据 Android Performance Benchmarking Guide,将响应时间从 300 ms 降低到 100 ms 可将用户留存率提高 25%。卡顿的诊断需要结合 CPU 和 Memory 性能分析以及垃圾回收频率分析。

要点概述

  • 卡顿 — 应用运行的不规则减速,与正常性能交替出现
  • 主要原因 — 频繁的GC暂停、UI线程中的同步操作、适配器中大量数据无分页
  • 诊断 需要 CPU Profiler 查找阻塞和 Memory Profiler 分析 GC 频率和持续时间
  • 解决 包括引入分页 (Paging 3)、通过 Room 优化 SQL 查询以及将繁重任务卸载到 WorkManager
  • 预防 — Benchmark Baseline Profiles、AOT 编译、最小化热路径中的分配

移动开发中的“卡顿”是什么意思

卡顿 — 用户用来描述主观感觉应用运行缓慢的非正式术语。与表现为持续延迟的延迟不同,卡顿是不规则的冻结:应用可以完美运行几秒钟,然后“思考”1–3 秒。

现象的技术特征

从性能分析的角度来看,卡顿 表现为一系列丢帧 (jank),峰值延迟超过 100 ms。在 FPS 图表上,这看起来像急剧下降:60 → 20 → 55 → 10 帧/秒。与 FPS 均匀较低的延迟不同,卡顿具有明显的变异性。

用户感知

当应用 卡顿 时,用户无法理解减速的逻辑:屏幕可以平滑滚动,然后突然停止一秒钟。这会引起挫败感并降低对应用的信任。根据 Google 的数据,53% 的用户会在加载时间超过 3 秒时离开网站或应用。

应用中突然变慢的原因

卡顿的不规则特性表明问题是由事件因素引起的,而不是持续过载。让我们来看典型的场景。

对象分配时的 GC 暂停

Android 的 ART 环境中,垃圾回收会停止应用的所有线程。如果代码中创建了大量临时对象——例如,每次调用 onBindViewHolder 通过连接创建新的 String——GC 会更频繁地启动。暂停时间取决于堆大小和对象代际,可能持续 5–50 ms。用户会感觉到突然的“思考”。

UI 线程中的同步 SQL 查询

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 — 循环中的字符串连接、热路径中的对象创建、没有 inSampleSize 的 Bitmap
  • iOS — 具有大量对象的自动释放池、没有缩放的 imageWithContentsOfFile、同步 URLSession
  • 跨平台 — UI 线程中的 JSON 解析、主线程中等待服务器响应的数据加载

如何在 Android 和 iOS 上诊断卡顿

不规则减速的诊断比恒定延迟的诊断更困难,因为问题可能不会在每次启动时重现。需要长时间收集统计数据。

Memory Profiler 及 GC 事件记录

Android Studio Memory Profiler 不仅显示内存使用情况,还显示 GC 事件:频率、类型(Concurrent、Full)、持续时间。如果在静止状态下 GC 每 5 秒发生超过 1 次——这是过度分配的迹象。在卡顿时记录堆转储可以看到哪些对象占用了内存。

使用 Allocation Tracking 的 Xcode Instruments

在 iOS 上使用 Instruments 中的 Allocations 模板来跟踪对象的创建和释放。启用代数 (Generations)——它们允许在操作之间拍摄堆快照并查看哪些对象留在内存中。未释放的持久对象——内存积累和后续暂停的来源。

Android 上的 JankStats API

JankStats — 实时收集丢帧指标的 Android 库。它将每个 jank 与当前场景(例如“列表滚动”、“打开屏幕”)关联起来,从而可以了解卡顿发生在哪个具体操作上。

在 Android 上集成 JankStats 跟踪卡顿的示例:

kotlin
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")
            }
        }
    }
}

消除运行缓慢的方法

消除卡顿需要针对每个原因进行有针对性的工作。没有通用解决方案——需要分析具体的性能概况。

通过 Paging 3 实现分页

如果列表包含 1000+ 个元素并且全部一次性加载——这保证会卡顿。Android 上的 Paging 3 和 iOS 上的 NSFetchedResultsController 在滚动时分块加载数据。用户只看到前 10–20 个元素,其余的在后台加载。

优化 SQL 查询和索引

Room 允许通过 Android Studio 中的 Inspection Tool 进行查询性能分析:可以看到执行时间、返回的行数和查询计划。在 WHERE 和 ORDER BY 列上添加索引可以将查询时间从 300 ms 减少到 5 ms。在 iOS 上,类似的检查由 Instruments 中的 Core Data Profiler 执行。

将任务卸载到 WorkManager

后台同步、文件上传、数据处理——所有这些都应该通过 WorkManager(Android)或 Background Tasks(iOS)执行。如果同步在 UI 线程中启动,应用将在执行期间卡顿。WorkManager 保证在后台线程中执行,同时考虑电池和网络状态。

在 Android 上通过 WorkManager 进行后台同步的示例:

kotlin
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()
        }
    }
}

开发阶段的卡顿预防

在编码阶段,通过遵循高效使用内存和线程的原则,可以预防卡顿。

用于 AOT 编译的 Baseline Profiles

Baseline Profiles — Android 预先 (AOT) 编译而不是 JIT 编译的类和方法列表。没有配置文件,每个新屏幕在第一次打开时都会被编译,导致 100–500 ms 的延迟。为关键屏幕准备 Baseline Profile 并通过 baseline-profile-gradle-plugin 在 Gradle 中启用生成。

最小化热路径中的分配

Hot path — 每帧执行的代码:onBindViewHolder、draw、layoutSubviews。避免在这些方法中创建对象:使用对象池、使用 StringBuilder 代替连接、缓存格式化字符串和格式化器。每次额外的分配都会使下一次 GC 更近。

在 CI 中通过 Baseline Profiles 进行性能分析

在 CI 流水线中添加运行 Macrobenchmark,使用列表滚动和屏幕打开的场景。设置阈值:第 99 百分位帧时间不应超过 16 ms。如果超过阈值——构建将被拒绝,直到优化完成。

  • Android — Baseline Profiles、Macrobenchmark、JankStats、带 penaltyDeath 的 StrictMode
  • iOS — MetricKit、os_signpost、XCTMetric、Debug 方案中的 Main Thread Checker
  • 通用方法 — 定期性能分析、关注热路径分配的代码审查

常见问题

卡顿与普通延迟有什么不同?

延迟 — 是恒定的延迟(例如,每次点击 200 ms)。卡顿 — 不规则的:应用正常运行,然后突然变慢 1–3 秒,然后再次恢复正常。原因 — 像 GC 暂停或数据库同步查询这样的事件因素。

如何在 Android 上测量 GC 暂停的频率?

使用 Android Studio 中的 Memory Profiler:Memory 选项卡显示带有持续时间的 GC 事件。对于生产监控,连接带有自定义跟踪的 Firebase Performance Monitoring。在 iOS 上启用 Malloc Debug 并在 Instruments 中标记分配代际。

网络请求会引起卡顿吗?

间接地——是的。如果服务器响应有延迟,而 UI 同步 等待它,应用会冻结。如果请求是异步的,但响应处理在 UI 线程中完成——这也会导致卡顿。解决方案——使用协程和进度指示器进行异步处理。

Kotlin Multiplatform 如何影响性能?

如果使用不当,KMP 可能会为互操作性生成过多的包装器对象。在 iOS 上,这会增加分配频率,从而增加 ARC 暂停。使用 @ObjCName,优化 expect/actual,避免从 UI 热路径频繁调用共享代码。

增加 Android 上的堆大小有帮助吗?

通过 android:largeHeap="true" 增加堆会延迟 GC,但不会消除分配的原因。当 GC 最终启动时,暂停 会更长,因为需要遍历更多对象。解决方案——减少分配数量,而不是扩大堆。

总结

  • 卡顿 — 由事件因素(GC 暂停、同步查询、图像解码)引起的应用不规则减速
  • 诊断 需要 Memory Profiler、Android 上的 JankStats 和 iOS 上 Instruments 中的 Allocation Tracking
  • 主要原因 — 频繁的 GC 暂停、缺少分页、非最优 SQL 查询和 UI 线程中的同步处理
  • 解决 — Paging 3、WorkManager、优化数据库索引、图像缩放和最小化分配
  • 预防 — Baseline Profiles、Macrobenchmark、StrictMode、检查热路径的代码审查
  • 工具 — JankStats、Firebase Performance、MetricKit 用于生产卡顿监控
  • 建议:在 CI 中实施定期的 Macrobenchmark 运行,帧的第 99 百分位阈值为 16 ms

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读