iOS 卡顿线上监控
一、核心原理主线程卡顿 RunLoop 超时监听kCFRunLoopBeforeSources / kCFRunLoopAfterWaiting两个状态超过 300ms 没反应 → 判定卡顿 → 抓堆栈 当前页面。测试结果用.dSYM看更详细的信息二、完整代码复制即用import UIKit import Foundation import MachO /// 主线程卡顿监控自动检测卡顿并输出具体方法名 final class CatonMonitor { static let shared CatonMonitor() private init() {} var currentPage 未知 var threshold: TimeInterval 0.3 private var isMonitoring false private var monitorThread: Thread? // MARK: - 启动 / 停止 func start() { guard !isMonitoring else { return } isMonitoring true monitorThread Thread { [weak self] in self?.monitorLoop() } monitorThread?.name com.caton.monitor monitorThread?.start() print(✅ 卡顿监控已启动阈值: \(Int(threshold * 1000))ms) } func stop() { isMonitoring false monitorThread?.cancel() monitorThread nil } // MARK: - 监控循环 private func monitorLoop() { while isMonitoring { var responded false DispatchQueue.main.async { responded true } Thread.sleep(forTimeInterval: threshold) if !responded { report(methods: symbolicateMainThreadStack()) while !responded isMonitoring { Thread.sleep(forTimeInterval: 0.1) } } Thread.sleep(forTimeInterval: 0.1) } } // MARK: - 输出报告 private func report(methods: [String]) { let list methods.map { → \($0) }.joined(separator: \n) let slide Self.appSlide() let addrs methods.compactMap { line - String? in guard let range line.range(of: #\[0x[0-9a-f]\]#, options: .regularExpression) else { return nil } return String(line[range]).dropFirst().dropLast().description }.joined(separator: ) let dsym Self.findDSYMPath() print( 卡顿报告 耗时≥\(Int(threshold * 1000)) ms 卡顿方法\(methods.isEmpty ? 系统内部调用 : \n\(list)) 还原行号终端执行: atos -o \(dsym) -arch arm64 -l \(slide) \(addrs) ) } /// 查找 dSYM 中包含业务代码的 DWARF 文件 private static func findDSYMPath() - String { let execName Bundle.main.executableURL?.lastPathComponent ?? YourApp let home NSHomeDirectory().components(separatedBy: /Library/).first ?? NSHomeDirectory() let derivedData \(home)/Library/Developer/Xcode/DerivedData let fm FileManager.default if let projects try? fm.contentsOfDirectory(atPath: derivedData) { for project in projects where project.hasPrefix(execName) { for config in [Debug-iphonesimulator, Debug-iphoneos, Release-iphonesimulator, Release-iphoneos] { let dwarfDir \(derivedData)/\(project)/Build/Products/\(config)/\(execName).app.dSYM/Contents/Resources/DWARF // 优先找 test.debug.dylibDebug 模式业务代码在这里 let dylib \(dwarfDir)/\(execName).debug.dylib if fm.fileExists(atPath: dylib) { return dylib } // 其次找主二进制 let main \(dwarfDir)/\(execName) if fm.fileExists(atPath: main) { return main } } } } return Bundle.main.executablePath ?? execName } /// 获取业务代码所在二进制的加载基地址 private static func appSlide() - String { let execName Bundle.main.executableURL?.lastPathComponent ?? // 优先找 test.debug.dylibDebug 模式业务代码在这里 for i in 0.._dyld_image_count() { if let name _dyld_get_image_name(i) { let path String(cString: name) if path.hasSuffix(\(execName).debug.dylib) { return String(format: 0x%lx, Int(bitPattern: _dyld_get_image_header(i))) } } } // 其次找主二进制 for i in 0.._dyld_image_count() { if let name _dyld_get_image_name(i), String(cString: name).hasSuffix(execName) { return String(format: 0x%lx, Int(bitPattern: _dyld_get_image_header(i))) } } return 0x0 } // MARK: - 符号化主线程调用栈 private func symbolicateMainThreadStack() - [String] { let addresses captureMainThreadStack() guard !addresses.isEmpty else { return [] } let appModule Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String ?? let execName Bundle.main.object(forInfoDictionaryKey: CFBundleExecutable) as? String ?? appModule let ignored [CatonMonitor, PageTrack, trackPage, thunk, entry_point] var results: [String] [] for addr in addresses { var info Dl_info() guard dladdr(addr, info) ! 0, let fname info.dli_fname, let sname info.dli_sname else { continue } let file (String(cString: fname) as NSString).lastPathComponent guard file execName || file.hasPrefix(execName .) || file appModule || file.hasPrefix(appModule .) else { continue } let name demangle(String(cString: sname)) if ignored.contains(where: { name.contains($0) }) { continue } if name main || name.hasSuffix(.$main) || name.hasSuffix(.main) { continue } let offset Int(bitPattern: addr) - Int(bitPattern: info.dli_saddr) let hex String(format: 0x%lx, Int(bitPattern: addr)) let desc \(name) \(offset) [\(hex)] if !results.contains(desc) { results.append(desc) } } return results } // MARK: - Mach API 遍历主线程栈帧 private func captureMainThreadStack() - [UnsafeRawPointer] { var list: thread_act_array_t? var count: mach_msg_type_number_t 0 guard task_threads(mach_task_self_, list, count) KERN_SUCCESS, let threads list, count 0 else { return [] } defer { vm_deallocate(mach_task_self_, vm_address_t(bitPattern: threads), vm_size_t(Int(count) * MemoryLayoutthread_act_t.size)) } let main threads[0] thread_suspend(main) defer { thread_resume(main) } #if arch(arm64) var state arm_thread_state64_t() var sc mach_msg_type_number_t(MemoryLayoutarm_thread_state64_t.size / MemoryLayoutnatural_t.size) let kr withUnsafeMutablePointer(to: state) { $0.withMemoryRebound(to: natural_t.self, capacity: Int(sc)) { thread_get_state(main, thread_state_flavor_t(ARM_THREAD_STATE64), $0, sc) } } guard kr KERN_SUCCESS else { return [] } let pc UnsafeRawPointer(bitPattern: UInt(state.__pc)) let fp UnsafeRawPointer(bitPattern: UInt(state.__fp)) #elseif arch(x86_64) var state x86_thread_state64_t() var sc mach_msg_type_number_t(MemoryLayoutx86_thread_state64_t.size / MemoryLayoutnatural_t.size) let kr withUnsafeMutablePointer(to: state) { $0.withMemoryRebound(to: natural_t.self, capacity: Int(sc)) { thread_get_state(main, thread_state_flavor_t(x86_THREAD_STATE64), $0, sc) } } guard kr KERN_SUCCESS else { return [] } let pc UnsafeRawPointer(bitPattern: UInt(state.__rip)) let fp UnsafeRawPointer(bitPattern: UInt(state.__rbp)) #else return [] #endif var addrs: [UnsafeRawPointer] [] if let pc pc { addrs.append(pc) } var cur fp while let f cur, addrs.count 128, isReadable(f) { let frame f.assumingMemoryBound(to: UnsafeRawPointer?.self) if let ra frame[1] { addrs.append(ra) } let next frame[0] if next nil || next cur { break } cur next } return addrs } private func isReadable(_ addr: UnsafeRawPointer) - Bool { var cnt: mach_msg_type_number_t 0 var data: vm_offset_t 0 let kr vm_read(mach_task_self_, vm_address_t(bitPattern: addr), vm_size_t(MemoryLayoutUnsafeRawPointer.size * 2), data, cnt) if kr KERN_SUCCESS { vm_deallocate(mach_task_self_, data, vm_size_t(cnt)); return true } return false } // MARK: - Demangle private func demangle(_ name: String) - String { name.withCString { cStr in guard let p _swift_demangle(cStr, UInt(strlen(cStr)), nil, nil, 0) else { return name } defer { free(p) } return String(cString: p) } } } _silgen_name(swift_demangle) private func _swift_demangle( _ mangledName: UnsafePointerCChar?, _ mangledNameLength: UInt, _ outputBuffer: UnsafeMutablePointerCChar?, _ outputBufferSize: UnsafeMutablePointerUInt?, _ flags: UInt32 ) - UnsafeMutablePointerCChar?三、使用方法import SwiftUI main struct testApp: App { init() { CatonMonitor.shared.start() } var body: some Scene { WindowGroup { ContentView() } } }四、测试卡顿struct ContentView: View { var body: some View { Button(模拟卡顿 (阻塞主线程)) { clickedButton() } } func clickedButton() { // ✅ 不需要任何手动埋点卡顿时自动抓取调用栈 print(开始模拟卡顿...) Thread.sleep(forTimeInterval: 2.0) print(卡顿结束) } }五、你能拿到什么线上定位神器卡顿耗时卡在哪个页面完整方法堆栈机型 系统六、面试必背为什么这套方案准因为UI 操作、事件响应、渲染全都在主线程 RunLoop 里。只要它超时就是真卡顿。七、项目落地把print换成网络上报配合dSYM 解析就能直接定位用户在哪一行代码卡住了。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2421904.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!