卡顿监测的核心是检测主线程是否被长时间阻塞,导致无法及时更新 UI。
卡顿的本质
帧率与刷新率
-
iOS 屏幕刷新率:60Hz(ProMotion 120Hz)
-
每帧理论时间:16.67ms(60Hz)或 8.33ms(120Hz)
-
卡顿定义:一帧画面渲染时间超过 16.67ms → 丢帧
VSync 信号
text
CPU/GPU 处理时间线:
[计算开始] → [提交渲染] → [VSync 信号] → [屏幕显示]
↓
如果这里 >16.67ms → 错过本次 VSync → 卡顿
卡顿监测的三种核心方法
1. FPS 监测法
最基础的卡顿指标,但不够精确。
class FPSMonitor {
private var displayLink: CADisplayLink?
private var lastTimestamp: TimeInterval = 0
private var count: Int = 0
private var fps: Int = 0
func start() {
displayLink = CADisplayLink(target: self, selector: #selector(tick))
displayLink?.add(to: .main, forMode: .common)
}
@objc func tick(_ link: CADisplayLink) {
guard lastTimestamp > 0 else {
lastTimestamp = link.timestamp
return
}
count += 1
let interval = link.timestamp - lastTimestamp
if interval >= 1.0 {
fps = count
count = 0
lastTimestamp = link.timestamp
if fps < 55 { // 通常 55fps 为卡顿阈值
print("⚠️ 低帧率警告: \(fps) FPS")
}
}
}
}
局限性:只能反映整体趋势,无法定位具体卡顿点。
2. 主线程 RunLoop 状态监测法(最常用)
核心原理:监控 RunLoop 每个循环的耗时。
RunLoop 工作原理
// RunLoop 的一次循环
while (1) {
// 1. 接收消息/事件 (Source0, Source1)
__CFRunLoopDoSources(runloop, mode, stopAfterHandle);
// 2. 处理定时器 (Timers)
__CFRunLoopDoTimers(runloop, mode);
// 3. UI 渲染 (渲染前)
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(observer, kCFRunLoopBeforeTimers);
// 4. 处理 UI 更新 (Source0)
// 这里耗时过长就会卡顿!
// 5. 渲染提交 (渲染后)
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(observer, kCFRunLoopBeforeWaiting);
// 6. 休眠,等待下一次唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer));
}
监测实现
class RunLoopMonitor {
private var timeoutCount = 0
private var runLoopActivity: CFRunLoopActivity = .entry
private var dispatchSemaphore: DispatchSemaphore?
private var runLoopObserver: CFRunLoopObserver?
private var monitoring = false
// 卡顿阈值(秒)
private let threshold: TimeInterval = 0.05 // 50ms,超过即判定为卡顿
func start() {
guard !monitoring else { return }
monitoring = true
// 创建信号量,用于同步
dispatchSemaphore = DispatchSemaphore(value: 0)
// 创建 RunLoop 观察者
let observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
CFRunLoopActivity.allActivities.rawValue,
true,
0
) { [weak self] (observer, activity) in
guard let self = self else { return }
// 记录当前 RunLoop 状态
self.runLoopActivity = activity
// 发送信号,唤醒监控线程
self.dispatchSemaphore?.signal()
}
runLoopObserver = observer
// 将观察者添加到主线程 RunLoop
CFRunLoopAddObserver(
CFRunLoopGetMain(),
observer,
CFRunLoopMode.commonModes
)
// 在子线程中监控超时
DispatchQueue.global().async { [weak self] in
self?.monitorRunLoop()
}
}
private func monitorRunLoop() {
guard let semaphore = dispatchSemaphore else { return }
while monitoring {
// 等待信号量,如果超时说明主线程卡住了
let result = semaphore.wait(timeout: .now() + threshold)
// 超时发生
if result == .timedOut {
// 排除正常运行的状态
if runLoopActivity == .beforeSources ||
runLoopActivity == .afterWaiting {
timeoutCount += 1
if timeoutCount < 2 {
continue // 忽略单次超时
}
// 连续超时,判定为卡顿
print("🚨 检测到卡顿!RunLoop 状态: \(runLoopActivity.rawValue)")
// 采集堆栈信息(关键!)
captureStackTrace()
}
} else {
timeoutCount = 0 // 正常执行,重置计数器
}
}
}
private func captureStackTrace() {
// 获取所有线程的堆栈
let symbols = Thread.callStackSymbols
// 过滤出主线程堆栈
DispatchQueue.main.async {
let mainThreadStack = Thread.callStackSymbols
print("主线程堆栈:\n\(mainThreadStack.joined(separator: "\n"))")
// 这里可以上报到监控系统
self.reportStutter(stackTrace: mainThreadStack)
}
}
func stop() {
monitoring = false
dispatchSemaphore = nil
if let observer = runLoopObserver {
CFRunLoopRemoveObserver(
CFRunLoopGetMain(),
observer,
CFRunLoopMode.commonModes
)
runLoopObserver = nil
}
}
}
3. 子线程 Ping 方法
原理:子线程定期"ping"主线程,检查是否及时响应。
class PingMonitor {
private var pingThread: Thread?
private var isMonitoring = false
private let pingInterval: TimeInterval = 0.05 // 50ms
private let timeoutThreshold: TimeInterval = 0.1 // 100ms
func start() {
isMonitoring = true
pingThread = Thread { [weak self] in
while self?.isMonitoring == true {
let startTime = Date()
// 向主线程发送任务
DispatchQueue.main.async {
self?.mainThreadResponded(at: startTime)
}
// 等待响应
Thread.sleep(forTimeInterval: self?.timeoutThreshold ?? 0.1)
// 检查是否超时
if let lastResponse = self?.lastResponseTime,
Date().timeIntervalSince(lastResponse) > self?.timeoutThreshold ?? 0.1 {
print("⚠️ 主线程响应超时")
self?.captureStackTrace()
}
Thread.sleep(forTimeInterval: self?.pingInterval ?? 0.05)
}
}
pingThread?.start()
}
private var lastResponseTime = Date()
private func mainThreadResponded(at time: Date) {
lastResponseTime = Date()
// 正常响应
}
}
卡顿根因分析
常见卡顿原因
// 1. 主线程同步网络请求 ❌
let data = try? Data(contentsOf: url) // 阻塞主线程
// 2. 复杂/大量的 UI 布局计算
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) {
// 复杂的 cell 布局计算
performComplexLayout() // 耗时 > 16ms
}
// 3. 大量文件/数据库操作
func saveLargeData() {
let data = Array(repeating: "data", count: 100000)
UserDefaults.standard.set(data, forKey: "large") // 序列化耗时
}
// 4. 死锁/竞争条件
DispatchQueue.main.sync { // 在主线程上同步执行 -> 死锁风险
updateUI()
}
// 5. 过度绘制/离屏渲染
view.layer.cornerRadius = 10
view.layer.masksToBounds = true // 触发离屏渲染
卡顿监控 SDK 设计
完整监控方案架构
class PerformanceMonitor {
// 多个监控维度
private let fpsMonitor = FPSMonitor()
private let runLoopMonitor = RunLoopMonitor()
private let memoryMonitor = MemoryMonitor()
private let cpuMonitor = CPUMonitor()
// 配置
struct Config {
var fpsThreshold: Int = 55
var stutterThreshold: TimeInterval = 0.05 // 50ms
var sampleRate: Float = 0.1 // 10%采样率
var enableStackTrace: Bool = true
}
func start(config: Config = Config()) {
// 开始各项监控
fpsMonitor.start(threshold: config.fpsThreshold)
runLoopMonitor.start(threshold: config.stutterThreshold)
// 设置采样率
if Float.random(in: 0...1) < config.sampleRate {
memoryMonitor.start()
cpuMonitor.start()
}
}
func reportStutter(stackTrace: [String]) {
// 1. 本地记录
saveToLocalCache(stackTrace)
// 2. 聚合上报(避免频繁上报)
aggregateAndReport()
// 3. 实时预警(可选)
if shouldAlert() {
showDeveloperWarning()
}
}
}
卡顿堆栈分析技巧
符号化与过滤
func analyzeStackTrace(_ stack: [String]) {
// 1. 过滤系统调用
let userFrames = stack.filter { !$0.contains("UIKitCore") &&
!$0.contains("libsystem") }
// 2. 提取关键函数
let keyFunctions = userFrames.compactMap { frame -> String? in
// 解析堆栈帧,提取函数名
let pattern = "\\s+\\d+\\s+(\\S+)\\s+(0x[0-9a-f]+)\\s+(.+)$"
if let regex = try? NSRegularExpression(pattern: pattern),
let match = regex.firstMatch(in: frame, range: NSRange(frame.startIndex..., in: frame)),
let range = Range(match.range(at: 3), in: frame) {
return String(frame[range])
}
return nil
}
// 3. 识别卡顿模式
analyzePattern(keyFunctions)
}
func analyzePattern(_ functions: [String]) {
// 常见卡顿模式识别
if functions.contains(where: { $0.contains("tableView:cellForRowAt:") }) {
print("🔍 卡顿原因:复杂 Cell 布局")
} else if functions.contains(where: { $0.contains("imageWithData:") }) {
print("🔍 卡顿原因:大图解码")
} else if functions.contains(where: { $0.contains("JSONSerialization.jsonObject") }) {
print("🔍 卡顿原因:JSON 解析")
}
}
优化建议
监控优化
-
采样率控制:生产环境使用低采样率(如 1%)
-
聚合上报:相同堆栈合并,避免数据爆炸
-
智能熔断:频繁相同卡顿降低监控频率
性能优化
// ✅ 优化示例
class OptimizedCell: UITableViewCell {
// 1. 异步图片加载
func loadImageAsync(url: URL) {
DispatchQueue.global().async {
let data = try? Data(contentsOf: url)
DispatchQueue.main.async {
self.imageView?.image = UIImage(data: data)
}
}
}
// 2. 缓存复杂计算结果
private var cachedHeight: CGFloat?
func cellHeight() -> CGFloat {
if let height = cachedHeight { return height }
let height = calculateComplexHeight()
cachedHeight = height
return height
}
// 3. 离屏渲染优化
func optimizeLayer() {
layer.cornerRadius = 10
layer.masksToBounds = true
layer.shouldRasterize = true // 开启光栅化
layer.rasterizationScale = UIScreen.main.scale
}
}
监控数据可视化
卡顿热力图
struct StutterReport {
let timestamp: Date
let duration: TimeInterval
let stackTrace: [String]
let deviceInfo: String
let pageName: String
// 转换为可上报格式
func toDictionary() -> [String: Any] {
return [
"type": "stutter",
"duration": duration,
"page": pageName,
"device": deviceInfo,
"stack": stackTrace.prefix(10).joined(separator: "\n"),
"timestamp": timestamp.timeIntervalSince1970
]
}
}
总结
| 监测方法 | 精度 | 开销 | 适用场景 |
|---|---|---|---|
| FPS 监测 | 低 | 低 | 整体趋势监控 |
| RunLoop 监测 | 高 | 中 | 精确卡顿定位 |
| Ping 方法 | 中 | 高 | 简单响应测试 |
最佳实践:
-
开发阶段:使用 RunLoop 监测 + 完整堆栈
-
测试阶段:结合自动化测试 + 性能 profiling
-
生产环境:采样监控 + 智能聚合上报
卡顿监测不是目的,优化用户体验才是根本。监测数据需要配合代码优化、架构改进才能真正提升 App 性能。