XXPerformanceMonitor是一個Swift版輕量卡頓監控工具,支持主線程和子線程,一句代碼便可輕鬆集成,開源在蝸牛的Github,能夠結合代碼來閱讀本文。ios
曾幾什麼時候跟項目大佬有過這樣的對話git
大佬:最近有用戶反饋用起來卡卡的,不太流暢,有找到緣由嗎? 蝸牛:沒找到,用戶的操做路徑太泛,沒有復現。github 大佬:那你想一想辦法如何監控線上卡頓吧。swift 蝸牛:.....api |
行吧,那就本身擼一個。緩存
這時候可能有小機靈舉手問了:國內主流集成平臺如友盟、聽雲、Bugly等均有卡頓監控,爲啥還要本身開發?安全
由於想裝逼。性能優化
開個玩笑,其實是由於公司項目處於隱私合規考慮,沒有使用國內平臺而使用了Fabric
,但它又沒有提供卡頓監控這部分功能,否則你覺得蝸牛閒的蛋疼→_→。bash
基於咱們的項目來看,用戶在使用上會感受到卡頓的場景,主要分爲兩種:微信
很明顯第一種狀況最爲致命,卡頓監測工具首先可以監測主線程阻塞,而且可以及時打印主線程上的方法棧,上傳到統計平臺便於開發者修復。
那麼接下來就有兩個問題須要考慮,如何監測線程阻塞和收集方法棧並上傳。
較早以前蝸牛就本身寫過runloop
版的實現,其實這也算是卡頓監控的標準答案,微信很早就是經過runloop
來實現的,可能有些同窗還看過那篇博文,後來微信推廣到了整個Bugly平臺。
此次有時間去作這個事情,並不想拿着之前的代碼再修修補補,通過一兩天的海選,最終有四種實現方式在等待亮燈:
fps | ping | runloop | hook msgSend | |
---|---|---|---|---|
卡頓反饋 | 高,但在table滑動、轉場動畫等狀況也會有下滑,會收集較多無用記錄 | 高,能有效收集主線程卡頓,且能夠控制卡頓閾值 | 中高,監控狀態切換耗時,但timer、dispatchMain、source1事件可能反饋不到位 | 極高,可能會採集到大量系統方法消耗 |
採集精度 | 低,需cpu空閒時才能回調,棧信息採集不夠及時 | 高,卡頓時能準確獲取到棧信息 | 中高 | 極高,只要是方法耗時,均會攔截 |
性能損耗 | 中低,閒置時會頻繁喚醒runloop處理 | 中,須要一個常駐子線程 | 低,僅監控runloop狀態 | 高,任意方法均會hook,處理量太大 |
開發成本 | 低,使用CADisplayLink實現 | 低,常駐子線程ping主線程,及時釋放臨時變量 | 中低,實現代碼相對較多 | 中高,依賴runtime,需使用OC編寫 |
fps
的方案可能有些同窗比較熟悉,由於這是一個監測頁面流暢度的比較常見的手段(有興趣的同窗能夠谷歌,爛大街了不必再寫),可是精度實在是比較低,而且不支持子線程,不符合咱們的要求。
runloop
的方式也不錯,可是最關鍵的一點,咱們平常的多線程開發,使用最多的是GCD
和OperationQueue
,這二者都是本身維護的線程池,咱們無法插手,想要監控子線程,還得使用Thread
來開發多線程,我選擇狗帶。
若是有同窗對
runloop
的方案感興趣,能夠移步iOS開發小記-RunLoop篇,在實際應用中有相關介紹及核心代碼。
hook msgSend
的方案是我惟一沒有實踐的,其實runtime
實現上問題卻是不大,可是一想到全部方法都被hook,而後先後添加耗時打印,程序一運行起來無很多天志滿屏飛就頭大,而且該方案肉眼可見的性能損耗,不予考慮。
ping
的方案卡頓反饋、採集精度都有不錯的表現,監控效果強,且性能損耗和開發成本較低,輕鬆支持全線程,徹底符合個人要求。
ping
的實現說白了就是線程同步,提供一個額外的worker線程去按期在目標線程裏修改全局狀態位,若是目標線程此時有空,必然能對標記位進行修改,若是worker線程超時發現標記位沒變,那麼能夠推測目標線程必然仍在處理其餘任務,此時上報全部線程的堆棧。
private final class WorkerThread: Thread {
// 監控間隔
private let threshold: CGFloat
// 捕獲閉包
private let catchHandler: () -> Void
// 信號量控制,避免重複上報
private let semaphore = DispatchSemaphore(value: 0)
// 遞歸鎖保證全局變量多線程安全
private let lockObj = NSObject()
private var _isResponse = true
private var isResponse: Bool {
get {
objc_sync_enter(lockObj)
let result = _isResponse
objc_sync_exit(lockObj)
return result
}
set {
objc_sync_enter(lockObj)
_isResponse = newValue
objc_sync_exit(lockObj)
}
}
init(_ t: CGFloat, _ handler: @escaping () -> Void) {
threshold = t
catchHandler = handler
super.init()
}
override func main() {
// 生命不息,監控不止
while !isCancelled {
// 及時釋放臨時變量
autoreleasepool {
// 全局標記位,實際上使用局部變量也能夠,只要注意OC語法下在block中修改須要對局部變量聲明__weak
isResponse = false
// 主線程同步標誌位,同時釋放信號量
DispatchQueue.main.async {
self.isResponse = true
self.semaphore.signal()
}
// 暫停指定間隔,檢驗此時標誌位是否修改,沒有修改則說明線程卡頓,須要上報
Thread.sleep(forTimeInterval: TimeInterval(threshold))
if !isResponse {
catchHandler()
}
// 避免重複上報,一次卡頓僅上報一次(這裏與微信runloop方案有比較大的區別,微信會按照斐波拉契間隔重複上報)
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
}
}
}
}
複製代碼
關於堆棧獲取的實現能夠移步戴銘-深刻剖析 iOS 性能優化,在這裏就再也不贅述。同窗們也能夠找找相關的開源庫或者按須要改改銘神的demo,應該比較容易,若是有須要,蝸牛本身擼的swift版整理下也能夠開源出來。
既然捕獲到了卡頓,在捕獲閉包裏,咱們須要獲取到全線程的堆棧信息而且上報
func handleThread(reason: String, domain: XXPerformanceMonitorDomain) {
// 1.獲取堆棧
// 2.上報
}
複製代碼
實際上咱們捕獲回調時仍然在worker線程中,因爲此時目標進程還在執行,想要更精確的結果,最好的方式就是暫停目標線程,保證此時捕獲的堆棧是準確的,這裏能夠經過pthread_kill
來實現,大體代碼以下:
// 註冊signal handle
signal(CALLSTACK_SIG, thread_singal_handler)
// 捕獲閉包中暫停主線程
func handleThread(reason: String, domain: XXPerformanceMonitorDomain) {
pthread_kill(threadID, CALLSTACK_SIG)
}
// 在signal handle中獲取堆棧信息
func thread_singal_handler(sig: int) {
// 捕獲當前堆棧
}
複製代碼
可是這裏當時遇到一個很是棘手的問題,因爲咱們上報數據使用的是Fabric的Non-fatals
上報,在thread_singal_handler
中調用相關API上傳堆棧地址數據時,老是收集到異常崩潰,因爲thread_singal_handler
中須要確保safe的調用,而翻閱官方文檔發現相關API在主線程多是不安全的,配合使用可能會致使偶現死鎖崩潰。
最終無奈放棄了pthread_kill
的方案選擇直接進行上報,實際上因爲堆棧地址獲取耗時並不明顯,直接上報形成的偏差實際上仍是能夠接受的。
問題1:捕獲回調能夠在目標線程中處理麼?
問題2:XXPerformanceMonitor中子線程監控僅支持
OperationQueue
,若是說不支持Thread
是因爲較少使用,那爲何GCD
也不支持?
首先明確監測到卡頓的落點堆棧,並不必定表明最後一個調用棧單個耗時就超過了閾值,它只是表示在整個方法執行中,執行到最後一個調用棧時已經超過了閾值,因此咱們須要根據堆棧信息的上下文來分析和判斷可能存在的卡頓點,不要只盯着最後一個調用棧分析。
它跟崩潰的強定位不一樣,更多的只是定位到可能存在的地方,用於輔助開發者去分析。以下圖
雖然最終定位的子方法c,但實際子方法b纔是真正形成卡頓的緣由。
這裏簡單舉幾個栗子🌰,有興趣的同窗歡迎留言補充:
項目裏爲了控制內存,在讀取圖片時,每每使用UIImage(contentsOfFile: )
,資源量一上來耗時每每超乎你的想象,針對該問題,有如下建議:
deinit
時手動釋放通常來講,文件io不建議在主線程操做,一是不支持文件的併發處理(多線程讀單線程寫),二是不方便管理,三是相對耗時。除非場景須要且沒法經過其餘方式實現,不然不要放在主線程。
另外主線程中同步等待文件IO也是個比較騷的操做,儘可能避免。
可能有兩種狀況:
文本計算單個操做耗時不算多,出現此類問題通常是使用問題,建議遇到該問題時,考慮以下兩點:
自己存在線程安全的類,在實現之初內部就應該管理好線程安全,而不是隨意讓外部調用者決定在哪條線程來執行,很是容易形成難以發現的崩潰。
通常開發時有兩種騷操做:
建議在修改時考慮:
若是咱們在開發中使用到了包含線程同步的方法,考慮下是否有主線程卡頓風險:
上線後經過數據收集和分析,發現了很多以前不易排查的卡頓點,而且經過兩三個版本的迭代優化,將主線程上卡頓率由1.1%降低至0.6%,減小45%,總體效果仍是使人滿意的。
原創不易,文章有任何錯誤,歡迎批(feng)評(kuang)指(diao)教(wo),順手點個贊,不甚感激! |