Jank — 这个术语指的是界面动画中明显的卡顿或“口吃”现象,由个别帧的丢失引起。在移动应用中,当帧的渲染时间超过显示刷新率分配的预算时,就会出现Jank。根据Android Developers, 2025,Jank是主观“卡顿”感的主要原因——应用可能在功能上完美,但由于FPS不稳定,用户会感觉它很慢。
要点
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的原因分为几类。第一类 — 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类型 | 原因 | 典型持续时间 | 查找工具 |
|---|---|---|---|
| Layout | requestLayout、relayout | 5–30毫秒 | Perfetto、Systrace |
| Draw | 过度绘制、沉重drawable | 3–20毫秒 | GPU Profiling |
| Thread | 主线程阻塞 | 10–200毫秒 | Android Studio Profiler |
| GC | 垃圾回收 | 5–15毫秒 | Memory Profiler |
| Rendering | GPU负载 | 10–50毫秒 | GPU Tracer、Xcode GPU |
在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即可。
Kotlin代码订阅Choreographer.FrameCallback并记录每个丢失的帧及其延迟时间。回调在每次VSync时调用。
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诊断通过使用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不稳定。
Swift代码通过CADisplayLink检测丢失的帧。如果timestamp和targetTimestamp之间的差异超过16.6毫秒——则记录为Jank。
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分析的既有操作系统的内置工具,也有第三方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时从系统接收警告。
Kotlin代码使用FrameMetricsAggregator按会话收集帧统计。停止聚合器后将显示丢失的帧数。
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回归。
Kotlin代码演示了在reportFullyDrawn之后异步加载数据到屏幕,以免繁重工作阻塞第一帧。回调在用户看到界面后被调用。
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 — 渲染帧丢失,表现为明显的动画卡顿或抖动。当帧准备时间超过时间预算(60 FPS时为16.6毫秒)时发生。
Layout Jank(频繁requestLayout)、Draw Jank(过度绘制)、Thread Jank(主线程阻塞)、GC Jank(垃圾回收)、IPC Jank(Binder调用)和Rendering Jank(沉重的着色器)。
使用Perfetto进行系统跟踪,GPU Profiling分析帧阶段,FrameMetricsAggregator进行生产环境监控。在Android Studio中——使用Deep Java Trace的CPU Profiler。
通过使用Core Animation或Metal System Trace模板的Instruments。对于生产环境——使用MXAnimatoryMetric的MetricKit。编程方式——CADisplayLink检查timestamp和targetTimestamp的差异。
根据Google的数据,Jank指标超过3%的滚动会话(每100次滚动中有3次包含卡顿)会导致负面评价增加22%。目标指标——低于0.5%的滚动会话。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。