iOS老司機帶你一起把App的崩潰率降到0.1%以下
持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第2天,點選檢視活動詳情
1. 前言: 如何把App的崩潰率降到0.1%以下?
- 崩潰無疑是我們在iOS開發工作中要面對的一個問題, 開發除錯階段的崩潰往往可以通過斷點排查處理; 線上的崩潰往往讓人手足無措, 需要結合Bugly等工具上傳符號表, 抽絲剝繭的尋找原因一併解決.
- 對於崩潰率, 0.1%往往是很多公司的硬性要求合格線, 在達到0.1%崩潰率的過程中, 我們作為一線iOS開發者, 可以做些什麼呢? 下面的思路和做法拋磚引玉, 歡迎大家在評論區交流探討:)
- 無痕植入的思路: AOP(面向切面程式設計)的思想. 基於OC的runtime執行時特性, 打點, 自動在App執行時實時捕獲導致App崩潰的因子, 然後通過針對性的的方法去應對因子, 做防崩處理.
2. 常見的8大崩潰產生原因
unrecognized selector
造成的崩潰: 沒有找到對應的方法選擇器.- KVO 造成的崩潰: KVO的被觀察者在
dealloc
時仍然註冊著KVO導致的崩潰, 重複新增觀察者或重複移除觀察者. - NSNotification 造成的崩潰: 當一個物件添加了
Notification
之後, 在dealloc
的時候, 仍然持有Notification
. - NSTimer 造成的崩潰: 需要在合適的時機
invalidate
定時器, 否則就會由於定時器的timer
強引用target
導致target
不被釋放, 造成記憶體洩漏. - 容器型別越界造成的崩潰:
Array越界、Dictionary插入nil
- 非主執行緒重新整理UI造成的崩潰: 在子執行緒重新整理UI會導致App崩潰.
- 野指標造成的崩潰: 訪問了野指標, 物件已經被釋放.
- 跟第三方合作時產生的崩潰: 三方只提供了基於.a靜態庫的SDK檔案, 三方更新後發生了崩潰
3. 常見的8大崩潰解決思路
3.1 unrecognized selector
造成的崩潰處理
- 採用攔截呼叫的方式, 在找不到呼叫的方法之後, App崩潰之前, 我們有機會通過重寫
NSObject
的四個訊息轉發方法來做防崩潰處理. ``` - (BOOL)resolveClassMethod:(SEL)sel; // 動態在方法決議機制, 決議類方法
- (BOOL)resolveInstanceMethod:(SEL)sel; // 動態的物件方法決議, 決議物件方法
// 後兩個方法需要轉發到其他的類處理
- (id)forwardingTargetForSelector:(SEL)aSelector; // 轉發給其它的一個物件去處理
- (void)forwardInvocation:(NSInvocation *)anInvocation; // 靈活地將目標函式以其他形式執行
``
- **攔截呼叫**的整個流程即OC的**訊息轉發機制**. runtime提供了3種方式去補救:
1. 呼叫
resolveInstanceMethod給個機會讓類新增這個函式實現.
- - 需要在類的本身動態地新增它不存在的方法, 這些方法對於該類是冗餘的.
2. 呼叫
forwardingTargetForSelector讓別的物件去執行這個函式.
- - 可以通過
NSInvocation的形式將訊息轉發給多個物件, 但是開銷比較大,
- - 需要建立新的
NSInvocation物件, 並且
forwardInvocation的函式經常被使用者呼叫來做**訊息的轉發選擇機制**, 不適合多次重寫.
3. 呼叫
forwardingInvocation(函式執行器)靈活地將目標函式以其他形式執行.
- - 可以將訊息轉發給一個同一物件, 開銷較小, 並且被重寫的概率較低, **推薦在這重寫.**
- 如果都不行, 系統才會呼叫
doesNotRecognizeSelector`丟擲異常.
- 重寫
NSObject
的forwardingTargetForSelector
具體步驟: - 為類動態地重建一個樁類.
- 動態為樁類新增對應的
Selector
, 用一個通用的返回0的函式來實現該SEL
的IMP
. - 將訊息直接轉發到這個樁類物件上. ```
-
(id)jh_forwardingTargetForSelector:(SEL)aSelector { if (class_respondsToSelector([self class], @selector(forwardInvocation:))) { IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:)); IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:)); if (imp != impOfNSObject) { NSLog(@"class has implemented invocation"); return nil; } }
JHUnrecognizedSelectorSolveObject solveObject = [JHUnrecoginzedSelectorSolveObject new]; solveObject.objc = self; return solveObject; }
`` - ps: 如果物件的類本身重寫了
forwardInvocation方法的話, 就不應該對
forwardingTargetForSelector`進行重寫了, 否則會影響到該型別的物件原本的訊息轉發流程*.
3.2 KVO 造成的崩潰處理
- 產生原因主要有2種
- KVO的被觀察者
dealloc
時仍然註冊著KVO導致的崩潰. - 新增KVO重複新增觀察者或重複移除觀察者導致的崩潰.
- 如上圖所示: 一個被觀察的物件有多個觀察者, 每個觀察者又有多個
keyPath
, - 如果觀察者和
keyPath
的數量一多, 很容易不清楚被觀察的物件整個KVO關係, - 導致被觀察者在
dealloc
的時候, 仍然殘存著一些關係沒有被登出, - 同時還會導致KVO註冊者和移除觀察者不匹配的情況發生,
-
尤其是多執行緒環境下, 導致KVO重複新增觀察者或者重複移除觀察者的情況, 這種類似的情況比較難排查.
-
可以這樣管理混亂的KVO關係:
- 讓觀察者物件持有一個KVO的
delegate
, 所有和KVO相關的操作均通過delegate
來進行管理, delegate
通過建立一張Map表來維護KVO的整個關係, 如下圖:
- 這樣做的好處如下:
1. 如果出現KVO重複新增或移除觀察者(KVO註冊者不匹配)的情況,
delegate
可以直接阻止這些異常操作.
2. 被觀察物件dealloc
之前, 可以通過delegate
自動將與自己有關的KVO關係都登出掉, 避免了KVO的被觀察者dealloc
時仍然註冊著KVO導致的崩潰.
3.3 NSNotification造成的崩潰處理
- iOS9之前, 當一個物件添加了
Notification
之後, 如果dealloc
的時候, 仍然持有Notification
, 就會出現NSNotification
型別的崩潰. - iOS9之後蘋果專門針對這種情況做了處理, 所以在iOS9之後, 即使開發者沒有移除Observer, Notification崩潰也不會再產生了.
- 針對iOS9之前的使用者, 防止
NSNotification
崩潰的思路是: - 利用
method swizzling hook NSObject
的dealloc
方法, - 在物件真正
dealloc
之前先呼叫一下[[NSNotificationCenter defaultCenter] removeObserve:self]
.
3.4 NSTimer記憶體洩漏造成的崩潰處理
- 產生原因: Runloop -> NSTimer --> <- - 物件 <-VC
- 這就導致了記憶體洩漏
- 處理方法如下:
NSTimer
和物件間新增一箇中間物件,NSTimer
強引用中間物件, 中間物件弱引用NSTimer
、物件
3.5 容器型別越界造成的崩潰處理
- 針對
NSArray、NSMutableArray、NSDictionary、NSMutableDictionary、NSCache
的一些常用的, 可能會導致崩潰的API進行基於runtime的method swizzling
, 然後在swizzle的新方法中針對Debug環境和Release加入一些判空處理操作, 從而讓這些API變得更難崩潰.
3.6 子執行緒重新整理UI造成的崩潰處理
- 採用基於runtime的
swizzle
UIView類的重新整理UI方法 ``` - (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect; ```
- 在自定義的交換方法裡, 呼叫上面幾個方法時, 判斷一下當前的執行緒, 如果不是主執行緒, 直接呼叫
dispatch_async(dispatch_get_main_queue(),^{// 原始碼});
, 來將對應的重新整理UI操作轉移到主執行緒來做, 也可統計錯誤資訊Debug模式下給到提示.
3.7 野指標造成的崩潰處理
- 當Bugly統計到
Exception Type:SIGSEGV, Exception Codes:SEGV_ACCERR
時, 就代表發生了野指標訪問. - 然而解決野指標造成的崩潰是一件比較棘手的事, 主要是因為崩潰資訊很難提供精準的定位, 這就導致野指標崩潰的場景不一定好復現.
- XCode為了開發階段除錯時就發現野指標問題, 提供了
Zombie
機制, 能夠在發生野指標時提示出現野指標的類, 從而解決了開發階段出現野指標的問題. - 但是線上環境產生的野指標問題, 依舊很難定位到具體的發生野指標的程式碼. 所以專門針對野指標做一層防崩措施, 在生產環境中就顯得很有必要. 常見的一個思路:
- 在類
init
初始化的時候做一個標記, 在該類dealloc
時再做一個標記. 通過2次的標記來判斷是否存在野指標. 但是對於UIVIew、UIImageView
這些常用的類來說, 多次分配釋放記憶體的CPU開銷還是很大的, 這只是一個思路. - 更推薦騰訊的MLeaksFinder.
- MLeaksFinder的思路:
MLeaksFinder一開始從UIViewController入手, 當一個UIViewController被pop或dismiss後, 該UIViewController包括他的view及subviews將很虧被釋放. 於是, 我們只需要在一個UIViewController被pop或dismiss一小段時間後, 看看這個UIViewController及它的view、subviews等是否還存在. MLeaksFinder具體的方法是為積累NSObject新增一個方法 -(void)willDealloc, 該方法的作用是: 先用一個弱指標指向self, 並在一小段時間後, 通過這個弱指標呼叫 -(void)assertNotDealloc, 而 assertNotDealloc主要作用是直接呼叫中斷言. 若果它沒被釋放(即發生了記憶體洩漏), assertNotDealloc就會被呼叫中斷言. 這樣一來, 當一個UIViewController被pop或dismiss時, 我們遍歷該UIViewController上所有的view, 依次呼叫 willDealloc, 若一小段時間(如2s)之後還沒釋放, 那麼指向它的weak指標還是存在的, 所以可以呼叫其tuntime繫結的方法 willDealloc 來提示野指標記憶體洩漏.
3.8 跟第三方合作時產生的崩潰處理
- 當公司跟第三方公司合作時, 第三方公司只提供了一個.a的SDK,
- 之前的版本可以穩定執行, 更新了第三方的SDK相關檔案後卻產生了線上的崩潰.
- 這種情況一般來說一旦出現就會非常緊急.
- 一般的解決思路是直接跟第三方聯絡, 讓他們再跑一下測試流程, 定位問題.
- 自己公司可以通過Bugly上收集到的崩潰資訊, 上傳符號表, 定位到崩潰的堆疊呼叫資訊.
- 聯合排查, 如果線上版本已經發布, 崩潰又比較緊急, 短時間內三方也排查不出問題.
- 這時可以通過Git分支的Tag, 回退到穩定版本, 緊急更新一個版本, 避免線上崩潰.
- 待三方公司排查出問題後, 更新三方SDK相關檔案, 再發一個bugFix版本.
發文不易, 喜歡點讚的人更有好運氣👍 :), 定期更新+關注不迷路~
ps:歡迎加入筆者18年建立的研究iOS稽核及前沿技術的三千人扣群:662339934,坑位有限,備註“掘金網友”可被群管通過~
- iOS老司機聊聊實際專案開發中的<<人月神話>>
- iOS老司機可落地在中大型iOS專案中的5大接地氣設計模式合集
- iOS老司機的跨端跨平臺Hybrid開發Tips
- iOS老司機的2022年回顧, 聊聊寒冬下的實用<<談判力>>
- iOS老司機可落地的中大型iOS專案中的設計模式優化Tips_橋接模式
- iOS老司機的多執行緒PThread學習分享
- iOS老司機整理, iOSer必會的經典演算法_2
- iOS老司機的<<藍海轉型>>讀書分享
- iOS老司機的<<程式設計師的自我修養:連結、裝載與庫>>讀書分享
- iOS老司機的接地氣演算法Tips
- iOS老司機的RunLoop原理探究及實用Tips
- iOS老司機整理, iOSer必會的經典演算法_1
- iOS老司機的App啟動優化Tips, 讓啟動速度提升10%
- iOS老司機的網路相關Tips
- 戀上資料結構與演算法
- iOS老司機帶你一起把App的崩潰率降到0.1%以下
- 探究Swift的String底層實現
- iOS老司機萬字整理, 可能是最全的Swift Tips
- iOS老司機可落地的中大型iOS專案中的設計模式優化Tips
- 聊一聊Swift中的閉包