如何用RunLoop检测iOS App 卡顿

2,299 阅读1分钟

首先本文的思路来自于网上的各种资料。然后搜了半天发现没有swift版的,于是撸了一个。

其实具体的思路非常的简单:

  1. 首先创建一个runloop的observer对象:

     let info = Unmanaged<Monitor>.passUnretained(self).toOpaque()
     var context: CFRunLoopObserverContext = CFRunLoopObserverContext(version: 0, info: info, retain: nil, release: nil, copyDescription: nil)
     self.runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0, runLoopCallBack(), &context)
     
    
  2. 然后将这个观察对象添加到runloop的common modes中

     CFRunLoopAddObserver(CFRunLoopGetCurrent(), self.runLoopObserver, CFRunLoopMode.commonModes)
     
    

    ps: 因为common modes是会一直存在于runloop中的,不会被中断,所以将检测的observer对象放到这个modes里去。

  3. 检测CFRunLoopActivity

    CFRunLoopActivity这个结构有非常多的状态吧,我们需要判断的是:

     beforeSources: 进入睡眠前
     afterWaiting: 唤醒后的状态
     
    

    如果runloop返回的activity的值是上述的两个,那么就可以认为出现了卡顿的现象

  4. 这里用了dispatch的信号机制

    self.dispatchSemaphore?.wait(timeout: DispatchTime.now() + 0.05)

    这段代码认定,如果连五次次超过了0.088秒,那么就认为发生了卡顿的现象

实现的逻辑就是这么四步,下面贴上全部的代码:

import Foundation

private let START_MONITOR_RATE = 88

class DJMonitor {

    static let shared = DJMonitor()
    
    var isMoniting = false
    var timeoutCount = 0
    var runLoopActivity: CFRunLoopActivity = .entry
    var dispatchSemaphore: DispatchSemaphore? = DispatchSemaphore(value: 0)
    
    var runloopObserver: CFRunLoopObserver?
    
    func start() {
        guard !isMoniting else {
            return
        }
        
        self.runloopObserver = buildRunLoopObserver()
        if self.runloopObserver == nil {
            print("创建监听失败...")
            return
        }
        
        isMoniting = true
        CFRunLoopAddObserver(CFRunLoopGetMain(), runloopObserver, CFRunLoopMode.commonModes)
        
        DispatchQueue.global().async {
            while true {
                let wait = self.dispatchSemaphore?.wait(timeout: DispatchTime.now() + 0.05)
                if DispatchTimeoutResult.timedOut == wait {
                    guard self.runloopObserver != nil else {
                        self.dispatchSemaphore = nil
                        self.runLoopActivity = .entry
                        return
                    }
                    
                    if self.runLoopActivity == .beforeSources || self.runLoopActivity == .afterWaiting {
                        if self.timeoutCount < 5 {
                            self.timeoutCount += 1
                            continue
                        }
                        
                        DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {
                            print("check the cpu info...")
                        }
                    }
                }
                
                self.timeoutCount = 0
            }
        }
        
    }
    
    func end() {
        guard self.runloopObserver != nil else {
            return
        }
        self.isMoniting = false
        CFRunLoopRemoveObserver(CFRunLoopGetMain(), self.runloopObserver, CFRunLoopMode.commonModes)
        self.runloopObserver = nil
    }
    
    private func buildRunLoopObserver() -> CFRunLoopObserver? {
        let info = Unmanaged<DJMonitor>.passUnretained(self).toOpaque()
        var context = CFRunLoopObserverContext(version: 0, info: info, retain: nil, release: nil, copyDescription: nil)
        let observer = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0, runLoopObserverCallback(), &context)
        return observer
    }
    
    
    func runLoopObserverCallback() -> CFRunLoopObserverCallBack {
        return { observer, activity, info in
            guard let info = info else {
                return
            }
            
            let weakSelf = Unmanaged<DJMonitor>.fromOpaque(info).takeUnretainedValue()
            weakSelf.runLoopActivity = activity
            weakSelf.dispatchSemaphore?.signal()
        }
    }
}