中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

EvenLoop模型在iOS的RunLoop怎么應用

發布時間:2022-07-20 13:56:05 來源:億速云 閱讀:124 作者:iii 欄目:開發技術

本文小編為大家詳細介紹“EvenLoop模型在iOS的RunLoop怎么應用”,內容詳細,步驟清晰,細節處理妥當,希望這篇“EvenLoop模型在iOS的RunLoop怎么應用”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。

    線程保活

    在實際開發中,我們通常會遇到常駐線程的創建,比如說發送心跳包,這就可以在一個常駐線程來發送心跳包,而不干擾主線程的行為,再比如音頻處理,這也可以在一個常駐線程中來處理。以前在Objective-C中使用的AFNetworking 1.0就使用了RunLoop來進行線程的保活。

    var thread: Thread!
    func createLiveThread() {
    		thread = Thread.init(block: {
    				let port = NSMachPort.init()
            RunLoop.current.add(port, forMode: .default)
            RunLoop.current.run()
    		})
    		thread.start()
    }

    值得注意的是RunLoop的mode中至少需要一個port/timer/observer,否則RunLoop只會執行一次就退出了。

    停止Runloop

    離開RunLoop一共有兩種方法:其一是給RunLoop配置一個超時的時間,其二是主動通知RunLoop離開。Apple在文檔中是推薦第一種方式的,如果能直接定量的管理,這種方式當然是最好的。

    設置超時時間

    然而實際中我們無法準確的去設置超時的時刻,比如在線程保活的例子中,我們需要保證線程的RunLoop一直保持運行中,所以結束的時間是一個變量,而不是常量,要達到這個目標我們可以結合一下RunLoop提供的API,在開始的時候,設置RunLoop超時時間為無限,但是在結束時,設置RunLoop超時時間為當前,這樣變相通過控制timeout的時間停止了RunLoop,具體代碼如下:

    var thread: Thread?
    var isStopped: Bool = false
    func createLiveThread() {
    		thread = Thread.init(block: { [weak self] in
    				guard let self = self else { return }
    				let port = NSMachPort.init()
            RunLoop.current.add(port, forMode: .default)
    				while !self.isStopped {
    		        RunLoop.current.run(mode: .default, before: Date.distantFuture)
            }
    		})
    		thread?.start()
    }
    func stop() {
    		self.perform(#selector(self.stopThread), on: thread!, with: nil, waitUntilDone: false)
    }
    @objc func stopThread() {
    		self.isStopped = true
    		RunLoop.current.run(mode: .default, before: Date.init())
        self.thread = nil
    }
    直接停止

    CoreFoundation提供了API:CFRunLoopStop() 但是這個方法只會停止當前這次循環的RunLoop,并不會完全停止RunLoop。那么有沒有其它的策略呢?我們知道RunLoop的Mode中必須要至少有一個port/timer/observer才會工作,否則就會退出,而CF提供的API中正好有:

    **public func CFRunLoopRemoveSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFRunLoopMode!)
    public func CFRunLoopRemoveObserver(_ rl: CFRunLoop!, _ observer: CFRunLoopObserver!, _ mode: CFRunLoopMode!)
    public func CFRunLoopRemoveTimer(_ rl: CFRunLoop!, _ timer: CFRunLoopTimer!, _ mode: CFRunLoopMode!)**

    所以很自然的聯想到如果移除source/timer/observer, 那么這個方案可不可以停止RunLoop呢?

    答案是否定的,這一點在Apple的官方文檔中有比較詳細的描述:

    Although removing a run loop’s input sources and timers may also cause the run loop to exit, this is not a reliable way to stop a run loop. Some system routines add input sources to a run loop to handle needed events. Because your code might not be aware of these input sources, it would be unable to remove them, which would prevent the run loop from exiting.

    簡而言之,就是你無法保證你移除的就是全部的source/timer/observer,因為系統可能會添加一些必要的source來處理事件,而這些source你是無法確保移除的。

    延遲加載圖片

    這是一個很常見的使用方式,因為我們在滑動scrollView/tableView/collectionView的過程,總會給cell設置圖片,但是直接給cell的imageView設置圖片的過程中,會涉及到圖片的解碼操作,這個就會占用CPU的計算資源,可能導致主線程發生卡頓,所以這里可以將這個操作,不放在trackingMode,而是放在defaultMode中,通過一種取巧的方式來解決可能的性能問題。

    func setupImageView() {
    		self.performSelector(onMainThread: #selector(self.setupImage), 
    												 with: nil, 
    												 waitUntilDone: false,
    												 modes: [RunLoop.Mode.default.rawValue])
    }
    @objc func setupImage() {
    		imageView.setImage()
    }

    卡頓監測

    目前來說,一共有三種卡頓監測的方案,然而基本上每一種卡頓監測的方案都和RunLoop是有關聯的。

    CADisplayLink(FPS)

    YYFPSLabel 采用的就是這個方案,FPS(Frames Per Second)代表每秒渲染的幀數,一般來說,如果App的FPS保持50~60之間,用戶的體驗就是比較流暢的,但是Apple自從iPhone支持120HZ的高刷之后,它發明了一種ProMotion的動態屏幕刷新率的技術,這種方式基本就不能使用了,但是這里依舊提供已作參考。

    這里值得注意的技術細節是使用了NSObject來做方法的轉發,在OC中可以使用NSProxy來做消息的轉發,效率更高。

    // 抽象的超類,用來充當其它對象的一個替身
    // Timer/CADisplayLink可以使用NSProxy做消息轉發,可以避免循環引用
    // swift中我們是沒發使用NSInvocation的,所以我們直接使用NSobject來做消息轉發
    class WeakProxy: NSObject {
        private weak var target: NSObjectProtocol?
        init(target: NSObjectProtocol) {
            self.target = target
            super.init()
        }
        override func responds(to aSelector: Selector!) -> Bool {
            return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)
        }
        override func forwardingTarget(for aSelector: Selector!) -> Any? {
            return target
        }
    }
    class FPSLabel: UILabel {
        var link: CADisplayLink!
        var count: Int = 0
        var lastTime: TimeInterval = 0.0
        fileprivate let defaultSize = CGSize.init(width: 80, height: 20)
        override init(frame: CGRect) {
            super.init(frame: frame)
            if frame.size.width == 0 || frame.size.height == 0 {
                self.frame.size = defaultSize
            }
            layer.cornerRadius = 5.0
            clipsToBounds = true
            textAlignment = .center
            isUserInteractionEnabled = false
            backgroundColor = UIColor.white.withAlphaComponent(0.7)
            link = CADisplayLink.init(target: WeakProxy.init(target: self), selector: #selector(FPSLabel.tick(link:)))
            link.add(to: RunLoop.main, forMode: .common)
        }
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        deinit {
            link.invalidate()
        }
        @objc func tick(link: CADisplayLink) {
            guard lastTime != 0 else {
                lastTime = link.timestamp
                return
            }
            count += 1
            let timeDuration = link.timestamp - lastTime
            // 1、設置刷新的時間: 這里是設置為1秒(即每秒刷新)
            guard timeDuration >= 1.0 else { return }
            // 2、計算當前的FPS
            let fps = Double(count)/timeDuration
            count = 0
            lastTime = link.timestamp
            // 3、開始設置FPS了
            let progress = fps/60.0
            let color = UIColor(hue: CGFloat(0.27 * (progress - 0.2)), saturation: 1, brightness: 0.9, alpha: 1)
            self.text = "\(Int(round(fps))) FPS"
            self.textColor = color
        }
    }

    子線程Ping

    這種方法是創建了一個子線程,通過GCD給主線程添加異步任務:修改是否超時的參數,然后讓子線程休眠一段時間,如果休眠的時間結束之后,超時參數未修改,那說明給主線程的任務并沒有執行,那么這就說明主線程的上一個任務還沒有做完,那就說明卡頓了,這種方式其實和RunLoop沒有太多的關聯,它不依賴RunLoop的狀態。在ANREye中是采用子線程Ping的方式來監測卡頓的。

    同時為了讓這些操作是同步的,這里使用了信號量。

    class PingMonitor {
        static let timeoutInterval: TimeInterval = 0.2
        static let queueIdentifier: String = "com.queue.PingMonitor"
        private var queue: DispatchQueue = DispatchQueue.init(label: queueIdentifier)
        private var isMonitor: Bool = false
        private var semphore: DispatchSemaphore = DispatchSemaphore.init(value: 0)
        func startMonitor() {
            guard isMonitor == false else { return }
            isMonitor = true
            queue.async {
                while self.isMonitor {
                    var timeout = true
                    DispatchQueue.main.async {
                        timeout = false
                        self.semphore.signal()
                    }
                    Thread.sleep(forTimeInterval:PingMonitor.timeoutInterval)
                    // 說明等了timeoutInterval之后,主線程依然沒有執行派發的任務,這里就認為它是處于卡頓的
                    if timeout == true {
                        //TODO: 這里需要取出崩潰方法棧中的符號來判斷為什么出現了卡頓
                        // 可以使用微軟的框架:PLCrashReporter
                    }
                    self.semphore.wait()
                }
            }
        }
    }

    這個方法在正常情況下會每隔一段時間讓主線程執行GCD派發的任務,會造成部分資源的浪費,而且它是一種主動的去Ping主線程,并不能很及時的發現卡頓問題,所以這種方法會有一些缺點。

    實時監控

    而我們知道,主線程中任務都是通過RunLoop來管理執行的,所以我們可以通過監聽RunLoop的狀態來知道是否會出現卡頓的情況,一般來說,我們會監測兩種狀態:第一種是kCFRunLoopAfterWaiting 的狀態,第二種是kCFRunLoopBeforeSource的狀態。為什么是兩種狀態呢?

    首先看第一種狀態kCFRunLoopAfterWaiting ,它會在RunLoop被喚醒之后回調這種狀態,然后根據被喚醒的端口來處理不同的任務,如果處理任務的過程中耗時過長,那么下一次檢查的時候,它依然是這個狀態,這個時候就可以說明它卡在了這個狀態了,然后可以通過一些策略來提取出方法棧,來判斷卡頓的代碼。同理,第二種狀態也是一樣的,說明一直處于kCFRunLoopBeforeSource 狀態,而沒有進入下一狀態(即休眠),也發生了卡頓。

    class RunLoopMonitor {
        private init() {}
        static let shared: RunLoopMonitor = RunLoopMonitor.init()
        var timeoutCount = 0
        var runloopObserver: CFRunLoopObserver?
        var runLoopActivity: CFRunLoopActivity?
        var dispatchSemaphore: DispatchSemaphore?
        // 原理:進入睡眠前方法的執行時間過長導致無法進入睡眠,或者線程喚醒之后,一直沒進入下一步
        func beginMonitor() {
            let uptr = Unmanaged.passRetained(self).toOpaque()
            let vptr = UnsafeMutableRawPointer(uptr)
            var context = CFRunLoopObserverContext.init(version: 0, info: vptr, retain: nil, release: nil, copyDescription: nil)
            runloopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                      CFRunLoopActivity.allActivities.rawValue,
                                                      true,
                                                      0,
                                                      observerCallBack(),
                                                      &context)
            CFRunLoopAddObserver(CFRunLoopGetMain(), runloopObserver, .commonModes)
            // 初始化的信號量為0
            dispatchSemaphore = DispatchSemaphore.init(value: 0)
            DispatchQueue.global().async {
                while true {
                    // 方案一:可以通過設置單次超時時間來判斷 比如250毫秒
    								// 方案二:可以通過設置連續多次超時就是卡頓 戴銘在GCDFetchFeed中認為連續三次超時80秒就是卡頓
                    let st = self.dispatchSemaphore?.wait(timeout: DispatchTime.now() + .milliseconds(80))
                    if st == .timedOut {
                        guard self.runloopObserver != nil else {
                            self.dispatchSemaphore = nil
                            self.runLoopActivity = nil
    												self.timeoutCount = 0
                            return
                        }
                        if self.runLoopActivity == .afterWaiting || self.runLoopActivity == .beforeSources {
    												self.timeoutCount += 1
                            if self.timeoutCount < 3 { continue }
                            DispatchQueue.global().async {
                                let config = PLCrashReporterConfig.init(signalHandlerType: .BSD, symbolicationStrategy: .all)
                                guard let crashReporter = PLCrashReporter.init(configuration: config) else { return }
                                let data = crashReporter.generateLiveReport()
                                do {
                                    let reporter = try PLCrashReport.init(data: data)
                                    let report = PLCrashReportTextFormatter.stringValue(for: reporter, with: PLCrashReportTextFormatiOS) ?? ""
                                    NSLog("------------卡頓時方法棧:\n \(report)\n")
                                } catch _ {
                                    NSLog("解析crash data錯誤")
                                }
                            }
                        }
                    }
                }
            }
        }
        func end() {
            guard let _ = runloopObserver else { return }
            CFRunLoopRemoveObserver(CFRunLoopGetMain(), runloopObserver, .commonModes)
            runloopObserver = nil
        }
        private func observerCallBack() -> CFRunLoopObserverCallBack {
            return { (observer, activity, context) in
                let weakself = Unmanaged<RunLoopMonitor>.fromOpaque(context!).takeUnretainedValue()
                weakself.runLoopActivity = activity
                weakself.dispatchSemaphore?.signal()
            }
        }
    }

    Crash防護

    Crash防護是一個很有意思的點,處于應用層的APP,在執行了某些不被操作系統允許的操作之后會觸發操作系統拋出異常信號,但是因為沒有處理這些異常從而被系操作系統殺掉的線程,比如常見的閃退。這里不對Crash做詳細的描述,我會在下一個模塊來描述iOS中的異常。要明確的是,有些場景下,是希望可以捕獲到系統拋出的異常,然后將App從錯誤中恢復,重新啟動,而不是被殺死。而對應在代碼中,我們需要去手動的重啟主線程,已達到繼續運行App的目的。

    let runloop = CFRunLoopGetCurrent()
    guard let allModes = CFRunLoopCopyAllModes(runloop) as? [CFRunLoopMode] else {
        return
    }
     while true {
    	  for mode in allModes {
            CFRunLoopRunInMode(mode, 0.001, false)
        }
     }

    CFRunLoopRunInMode(mode, 0.001, false) 因為無法確定RunLoop到底是怎樣啟動的,所以采用了這種方式來啟動RunLoop的每一個Mode,也算是一種替代方案了。因為CFRunLoopRunInMode 在運行的時候本身就是一個循環并不會退出,所以while循環不會一直執行,只是在mode退出之后,while循環遍歷需要執行的mode,直到繼續在一個mode中常駐。

    這里只是重啟RunLoop,其實在Crash防護里最重要的還是要監測到何時發送崩潰,捕獲系統的exception信息,以及singal信息等等,捕獲到之后再對當前線程的方法棧進行分析,定位為crash的成因。

    Matrix框架

    接下來我們具體看一下RunLoop在Matrix框架中的運用。Matrix是騰訊開源的一款用于性能監測的框架,在這個框架中有一款插件**WCFPSMonitorPlugin:**這是一款FPS監控工具,當用戶滑動界面時,記錄主線程的調用棧。它的源碼中和我們上述提到的通過CADisplayLink來來監測卡頓的方案的原理是一樣的:

    - (void)startDisplayLink:(NSString *)scene {
        FPSInfo(@"startDisplayLink");
        m_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onFrameCallback:)];
        [m_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    		...
    }
    - (void)onFrameCallback:(id)sender {
        // 當前時間: 單位為秒
        double nowTime = CFAbsoluteTimeGetCurrent();
        // 將單位轉化為毫秒
        double diff = (nowTime - m_lastTime) * 1000;
    		// 1、如果時間間隔超過最大的幀間隔:那么此次屏幕刷新方法超時
        if (diff > self.pluginConfig.maxFrameInterval) {
            m_currRecorder.dumpTimeTotal += diff;
            m_dropTime += self.pluginConfig.maxFrameInterval * pow(diff / self.pluginConfig.maxFrameInterval, self.pluginConfig.powFactor);
            // 總超時時間超過閾值:展示超時信息
            if (m_currRecorder.dumpTimeTotal > self.pluginConfig.dumpInterval * self.pluginConfig.dumpMaxCount) {
                FPSInfo(@"diff %lf exceed, begin: %lf, end: %lf, scene: %@, you can see more detail in record id: %d",
                        m_currRecorder.dumpTimeTotal,
                        m_currRecorder.dumpTimeBegin,
                        m_currRecorder.dumpTimeBegin + m_currRecorder.dumpTimeTotal / 1000.0,
                        m_scene,
                        m_currRecorder.recordID);
    						...... 
            }
    		// 2、如果時間間隔沒有最大的幀間隔:那么此次屏幕刷新方法不超時
        } else {
            // 總超時時間超過閾值:展示超時信息
            if (m_currRecorder.dumpTimeTotal > self.pluginConfig.maxDumpTimestamp) {
                FPSInfo(@"diff %lf exceed, begin: %lf, end: %lf, scene: %@, you can see more detail in record id: %d",
                        m_currRecorder.dumpTimeTotal,
                        m_currRecorder.dumpTimeBegin,
                        m_currRecorder.dumpTimeBegin + m_currRecorder.dumpTimeTotal / 1000.0,
                        m_scene,
                        m_currRecorder.recordID);
    						....
    				// 總超時時間不超過閾值:將時間歸0 重新計數
            } else {
                m_currRecorder.dumpTimeTotal = 0;
                m_currRecorder.dumpTimeBegin = nowTime + 0.0001;
            }
        }
        m_lastTime = nowTime;
    }

    它通過次數以及兩次之間允許的時間間隔作為閾值,超過閾值就記錄,沒超過閾值就歸0重新計數。當然這個框架也不僅僅是作為一個簡單的卡頓監測來使用的,還有很多性能監測的功能以供平時開發的時候來使用:包括對崩潰時方法棧的分析等等。

    讀到這里,這篇“EvenLoop模型在iOS的RunLoop怎么應用”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    河东区| 广汉市| 赞皇县| 凤冈县| 嘉定区| 砀山县| 永安市| 金溪县| 北辰区| 肇庆市| 嘉禾县| 盐亭县| 永清县| 独山县| 长治县| 万州区| 尼勒克县| 轮台县| 增城市| 噶尔县| 彰化市| 永康市| 松溪县| 谷城县| 綦江县| 吴川市| 梧州市| 五寨县| 敖汉旗| 瓦房店市| 勃利县| 寿宁县| 汤阴县| 澄江县| 兴安县| 阿巴嘎旗| 诸城市| 沧源| 楚雄市| 龙口市| 铁岭县|