移动应用的Jank——是什么、原因与消除方法

作者: IT Sectr 发布日期: 2026-04-01 阅读时间: 10 分钟

Jank — 这个术语指的是界面动画中明显的卡顿或“口吃”现象,由个别帧的丢失引起。在移动应用中,当帧的渲染时间超过显示刷新率分配的预算时,就会出现Jank。根据Android Developers, 2025Jank是主观“卡顿”感的主要原因——应用可能在功能上完美,但由于FPS不稳定,用户会感觉它很慢。

要点

  • Jank — 丢失的帧,表现为动画卡顿。
  • 主要原因 — 超出每帧时间预算(60 FPS时为16.6毫秒)。
  • Jank由沉重的Layout、长时间GC、主线程阻塞或过度绘制引起。
  • 诊断使用FrameTimeline(Android)和Instruments(iOS)。
  • 消除Jank可将NPS和用户留存率提高15–25%。

什么是Jank

Jank — 是计算机图形学中的一个术语,指动画不是平滑滑动而是抖动运动的视觉缺陷。在移动开发中,Jank以单位时间内丢失的帧数(skipped frames)来衡量。如果系统未能在VSync时刻之前准备好帧,显示器会重复上一帧——在60Hz下产生16.6毫秒的暂停。一个丢失的帧可能不易察觉,但连续3–5个丢失的帧会产生50–80毫秒的“卡顿”感,用户可以清楚地感知到。

Jank对于必须以恒定速度运行的动画尤为关键:列表滚动、菜单打开动画、视差效果、屏幕间过渡。根据Google的UX研究(2024年),Jank指标超过3%滚动会话的应用比指标低于0.5%的应用多获得22%的一星评价。Android Vitals工具会自动跟踪Jank并根据严重程度分类:moderate、severe和critical。

Jank的主要原因

Jank的原因分为几类。第一类 — Layout Jank:由View尺寸变化、LayoutTransition动画或动态内容加载引起的频繁requestLayout()调用导致。每次requestLayout调用都会触发整个View子树的Measure + Layout,可能需要5–30毫秒。第二类 — Draw Jank:与过度绘制(overdraw)和使用沉重的drawable有关。第三类 — Thread Jank:由于同步操作(文件加载、在主线程上操作数据库、解码Bitmap)导致的主线程阻塞。

第四类 — GC Jank:ART/Dalvik或Swift ARC中的垃圾回收。当堆中积累了大量对象时,GC会触发5–15毫秒的Stop-The-World暂停。在Android中,GC暂停最常发生在循环中的频繁分配时:在onDraw()中创建对象、在适配器中分配、未使用的lambda表达式。第五类 — IPC Jank:主线程上的进程间通信(ContentProvider、Binder)。第六类 — Rendering Jank:由于非最佳着色器或大尺寸纹理导致的GPU渲染缓慢。

Jank类型原因典型持续时间查找工具
LayoutrequestLayout、relayout5–30毫秒Perfetto、Systrace
Draw过度绘制、沉重drawable3–20毫秒GPU Profiling
Thread主线程阻塞10–200毫秒Android Studio Profiler
GC垃圾回收5–15毫秒Memory Profiler
RenderingGPU负载10–50毫秒GPU Tracer、Xcode GPU

Android中的Jank诊断

在Android中,Jank诊断从系统跟踪Perfetto开始。Perfetto记录所有线程、CPU、GPU和调度器的活动。Jank的明显指标是Choreographer.doFrame和Choreographer.doCallbacks行:如果两次连续doFrame调用之间的间隔超过16.6毫秒,则帧已丢失。Perfetto显示确切的原因——哪个系统调用、锁或GC导致了延迟。在Android Studio Profiler中,类似功能可通过CPU Profiler获得。

用于生产环境中自动检测Jank的是FrameMetricsAggregator——一个收集每帧统计并按会话聚合的API。在Android 12+中出现了PerformanceHintManager——用于向系统提示目标帧率的API。如果应用指示它在120 FPS场景下工作,系统可以提高CPU/GPU频率以防止Jank。要简单记录所有丢失的帧,只需订阅Choreographer.FrameCallback即可。

通过Choreographer记录Jank

Kotlin代码订阅Choreographer.FrameCallback并记录每个丢失的帧及其延迟时间。回调在每次VSync时调用。

kotlin
class JankDetector {

    private val frameBudget = 16_666_666L
    private var previousFrameTime = 0L

    private val callback =
        Choreographer.FrameCallback { currentTime ->
            if (previousFrameTime != 0L) {
                val frameDuration =
                    currentTime - previousFrameTime
                val skippedFrames =
                    (frameDuration / frameBudget) - 1
                if (skippedFrames > 0) {
                    Log.w("Jank",
                        "跳过了$skippedFrames帧")
                }
            }
            previousFrameTime = currentTime
            Choreographer.getInstance()
                .postFrameCallback(this)
        }

    fun start() {
        Choreographer.getInstance()
            .postFrameCallback(callback)
    }
}

iOS中的Jank诊断

在iOS中,Jank诊断通过使用Core Animation模板的Instruments进行。Instruments显示实时FPS、离屏渲染数量和命中测试。iOS中Jank的主要指标:Core Animation时间轴上的红色柱(超出帧预算)、高Renderer指标(表示离屏渲染)和低FPS。对于生产环境监控,MetricKit收集包含MXAnimatoryMetric指标的报告,包括平均FPS、P50和P95帧时间。

iOS中原生的Jank诊断包括CADisplayLink以及timestamp和targetTimestamp检查。如果当前timestamp明显落后于targetTimestamp,则表示一个或多个帧已丢失。Apple还建议使用os_signpost进行自定义分析:在帧渲染的开始和结束处放置signpost-interval,并在Instruments中检查哪些间隔超过16.6毫秒。在SwiftUI中,用于诊断Jank的是UIView.invalidateIntrinsicContentSize——频繁调用此方法表明Layout不稳定。

用于检测Jank的CADisplayLink

Swift代码通过CADisplayLink检测丢失的帧。如果timestamp和targetTimestamp之间的差异超过16.6毫秒——则记录为Jank。

swift
class JankMonitor {

    private var displayLink: CADisplayLink?
    private var totalJank = 0

    func start() {
        displayLink = CADisplayLink(
            target: self,
            selector: #selector(detectJank)
        )
        displayLink?.add(to: .current,
            forMode: .common)
    }

    @objc
    private func detectJank() {
        guard let link = displayLink else { return }
        let delay = link.targetTimestamp
            - link.timestamp
        if delay > 0.0167 {
            totalJank += 1
        }
    }
}

Jank分析工具

用于Jank分析的既有操作系统的内置工具,也有第三方SDK。在Android中,关键工具是Perfetto(取代了Systrace)。Perfetto允许记录长达30秒的跟踪并通过Web界面ui.perfetto.dev进行分析。它显示带有Choreographer活动、渲染线程(RenderThread)和GPU的精确时间轴。对于GPU问题的详细分析,使用AGI(Android GPU Inspector),它不仅可以显示帧时间,还可以显示特定GPU块——着色器、光栅化器、纹理块的负载。

在iOS中,对应的是使用Core Animation、Metal System Trace和GPU Driver模板的Instruments。Core Animation显示FPS和帧时间,Metal System Trace显示GPU活动,精细到每个draw call。在真实设备上进行负载分析,使用Firebase Performance(收集Screen Rendering指标)和Sentry(在Jank时捕获堆栈跟踪)。新的Android 15 Performance Hint API允许开发者向系统指示哪些帧重要,并在接近Jank时从系统接收警告。

生产环境中的FrameMetricsAggregator

Kotlin代码使用FrameMetricsAggregator按会话收集帧统计。停止聚合器后将显示丢失的帧数。

kotlin
class JankAggregator(private val activity: Activity) {

    private val aggregator = FrameMetricsAggregator()

    fun startCollection() {
        aggregator.add(activity.window)
    }

    fun stopAndReport() {
        aggregator.remove()
        val result = aggregator.getMetrics()
        val totalFrames = result
            ?.get(FrameMetrics.TOTAL_DURATION)
            ?.size ?: 0
        val jankFrames = result
            ?.get(FrameMetrics.TOTAL_DURATION)
            ?.count { it > 16_666_666L} ?: 0
        Log.d("JankReport",
            "Jank比率:${jankFrames * 100 / totalFrames}%")
    }
}

消除帧卡顿的方法

消除Jank需要根据其类型组合方法。对于Layout Jank:用ConstraintLayout/Compose/SwiftUI替换深层层次结构,使用合并标签,避免在动画中使用requestLayout。对于Draw Jank:使用Debug GPU Overdraw查找4x+过度绘制,用矢量图(VectorDrawable/PDF)替换沉重的drawable,谨慎使用硬件层——它们加速渲染但消耗更多GPU内存。对于Thread Jank:将所有I/O操作、数据库工作和Bitmap解码移到后台线程,使用带有正确Dispatcher的Kotlin Coroutines或带有Schedulers.io()的RxJava。

对于GC Jank:最小化onDraw()和getView()中的分配,使用对象池(ObjectPool),用索引for替换for-each,谨慎使用Kotlin中的immutable data class和copy()——copy会创建新对象。对于IPC Jank:通过App Startup延迟初始化ContentProvider,将Binder调用移到后台线程。对于Rendering Jank:将纹理大小减小到屏幕最大分辨率,使用ASTC或ETC2压缩,避免不必要的着色器编译(提前编译着色器)。综合解决方案——在CI中定期运行Perfetto/Instruments分析并跟踪Jank回归。

反Jank模式:异步布局

Kotlin代码演示了在reportFullyDrawn之后异步加载数据到屏幕,以免繁重工作阻塞第一帧。回调在用户看到界面后被调用。

kotlin
class JankSafeLoader {

    suspend fun loadAfterFirstFrame(
        activity: Activity
    ) {
        // 保证第一帧已渲染
        if (Build.VERSION.SDK_INT >= 29) {
            activity.reportFullyDrawn()
        }

        // 繁重加载——在第一帧之后
        withContext(Dispatchers.IO) {
            val data = fetchHeavyData()
            withContext(Dispatchers.Main) {
                updateUI(data)
            }
        }
    }
}

常见问题

移动应用中的Jank是什么?

Jank — 渲染帧丢失,表现为明显的动画卡顿或抖动。当帧准备时间超过时间预算(60 FPS时为16.6毫秒)时发生。

Jank的主要原因是什么?

Layout Jank(频繁requestLayout)、Draw Jank(过度绘制)、Thread Jank(主线程阻塞)、GC Jank(垃圾回收)、IPC Jank(Binder调用)和Rendering Jank(沉重的着色器)。

如何在Android中诊断Jank?

使用Perfetto进行系统跟踪,GPU Profiling分析帧阶段,FrameMetricsAggregator进行生产环境监控。在Android Studio中——使用Deep Java Trace的CPU Profiler。

如何在iOS中测量Jank?

通过使用Core Animation或Metal System Trace模板的Instruments。对于生产环境——使用MXAnimatoryMetric的MetricKit。编程方式——CADisplayLink检查timestamp和targetTimestamp的差异。

Jank的百分比达到多少被认为是关键的?

根据Google的数据,Jank指标超过3%的滚动会话(每100次滚动中有3次包含卡顿)会导致负面评价增加22%。目标指标——低于0.5%的滚动会话。

总结

  • Jank — 丢失的帧,导致移动应用中可见的动画卡顿。
  • 主要原因:Layout Jank、Draw Jank、Thread Jank、GC Jank、IPC Jank和Rendering Jank。
  • Android中的Jank诊断——通过Perfetto、GPU Profiling和FrameMetricsAggregator。
  • iOS中的Jank诊断——通过Instruments、CADisplayLink和MetricKit。
  • 消除Jank需要组合:扁平层次结构、后台线程、最小化分配、缓存。
  • 目标Jank指标——低于0.5%的滚动会话出现卡顿。
  • 在CI中定期进行分析可防止性能回归进入生产环境。

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

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

讨论项目

另请阅读