卡顿监测原理

卡顿监测的核心是检测主线程是否被长时间阻塞,导致无法及时更新 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. 采样率控制:生产环境使用低采样率(如 1%)

  2. 聚合上报:相同堆栈合并,避免数据爆炸

  3. 智能熔断:频繁相同卡顿降低监控频率

性能优化

复制代码
// ✅ 优化示例
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 方法 简单响应测试

最佳实践

  1. 开发阶段:使用 RunLoop 监测 + 完整堆栈

  2. 测试阶段:结合自动化测试 + 性能 profiling

  3. 生产环境:采样监控 + 智能聚合上报

卡顿监测不是目的,优化用户体验才是根本。监测数据需要配合代码优化、架构改进才能真正提升 App 性能。

相关推荐
@大迁世界5 小时前
iOS 26.2 引入三种全新 iPhone 自定义方式
ios·iphone
Sheffi665 小时前
iOS 触摸事件完整传递链路:Hit-Test 全流程深度解析
macos·ios·cocoa
Swift社区6 小时前
用 Task Local Values 构建 Swift 里的依赖容器:一种更轻量的依赖注入思路
开发语言·ios·swift
2501_915909066 小时前
苹果应用加密方案的一种方法,在没有源码的前提下,如何处理 IPA 的安全问题
android·安全·ios·小程序·uni-app·iphone·webview
TouchWorld6 小时前
iOS逆向-哔哩哔哩增加3倍速播放(4)- 竖屏视频·全屏播放场景
ios·swift
2501_915909067 小时前
iOS 项目中常被忽略的 Bundle ID 管理问题
android·ios·小程序·https·uni-app·iphone·webview
2501_915921437 小时前
iOS App 测试的工程化实践,多工具协同的一些尝试
android·ios·小程序·https·uni-app·iphone·webview