卡顿监测原理

卡顿监测的核心是检测主线程是否被长时间阻塞,导致无法及时更新 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 性能。

相关推荐
YongPagani1 天前
Mac安装Homebrew
macos
Byron Loong1 天前
【系统】Mac系统和Linux 指令对比
linux·macos·策略模式
软件小滔1 天前
拖拽出来的专业感
经验分享·macos·mac·应用推荐
搜狐技术产品小编20231 天前
精通 UITableViewDiffableDataSource——从入门到重构的现代 iOS 列表开发指南
ios·重构
coooliang1 天前
Macos下载元神 ipa文件
macos
Benny的老巢1 天前
【n8n工作流入门02】macOS安装n8n保姆级教程:Homebrew与npm两种方式详解
macos·npm·node.js·n8n·n8n工作流·homwbrew·n8n安装
tangweiguo030519871 天前
SwiftUI 状态管理完全指南:从 @State 到 @EnvironmentObject
ios
声网1 天前
如何用 Fun-ASR-Nano 微调一个「听懂行话」的语音模型?丨Voice Agent 学习笔记
笔记·学习·xcode
望眼欲穿的程序猿1 天前
基于Linux&MacOS 开发Ai8051U
linux·运维·macos
TESmart碲视2 天前
M4芯片MacBook支持多显示器吗?mac如何与KVM切换器使用。
macos·计算机外设·mst·kvm切换器·双屏kvm切换器