iOS老司機帶你一起把App的崩潰率降到0.1%以下

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第2天,點選檢視活動詳情

1. 前言: 如何把App的崩潰率降到0.1%以下?

  • 崩潰無疑是我們在iOS開發工作中要面對的一個問題, 開發除錯階段的崩潰往往可以通過斷點排查處理; 線上的崩潰往往讓人手足無措, 需要結合Bugly等工具上傳符號表, 抽絲剝繭的尋找原因一併解決.
  • 對於崩潰率, 0.1%往往是很多公司的硬性要求合格線, 在達到0.1%崩潰率的過程中, 我們作為一線iOS開發者, 可以做些什麼呢? 下面的思路和做法拋磚引玉, 歡迎大家在評論區交流探討:)
  • 無痕植入的思路: AOP(面向切面程式設計)的思想. 基於OC的runtime執行時特性, 打點, 自動在App執行時實時捕獲導致App崩潰的因子, 然後通過針對性的的方法去應對因子, 做防崩處理.

2. 常見的8大崩潰產生原因

  1. unrecognized selector造成的崩潰: 沒有找到對應的方法選擇器.
  2. KVO 造成的崩潰: KVO的被觀察者在dealloc時仍然註冊著KVO導致的崩潰, 重複新增觀察者或重複移除觀察者.
  3. NSNotification 造成的崩潰: 當一個物件添加了Notification之後, 在dealloc的時候, 仍然持有Notification.
  4. NSTimer 造成的崩潰: 需要在合適的時機invalidate定時器, 否則就會由於定時器的timer強引用target導致target不被釋放, 造成記憶體洩漏.
  5. 容器型別越界造成的崩潰: Array越界、Dictionary插入nil
  6. 非主執行緒重新整理UI造成的崩潰: 在子執行緒重新整理UI會導致App崩潰.
  7. 野指標造成的崩潰: 訪問了野指標, 物件已經被釋放.
  8. 第三方合作時產生的崩潰: 三方只提供了基於.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`丟擲異常.

  • 重寫NSObjectforwardingTargetForSelector具體步驟:
  • 為類動態地重建一個樁類.
  • 動態為樁類新增對應的Selector, 用一個通用的返回0的函式來實現該SELIMP.
  • 將訊息直接轉發到這個樁類物件上. ```
  • (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重複新增觀察者或重複移除觀察者導致的崩潰.

image.png

  • 如上圖所示: 一個被觀察的物件有多個觀察者, 每個觀察者又有多個keyPath,
  • 如果觀察者和keyPath的數量一多, 很容易不清楚被觀察的物件整個KVO關係,
  • 導致被觀察者在dealloc的時候, 仍然殘存著一些關係沒有被登出,
  • 同時還會導致KVO註冊者和移除觀察者不匹配的情況發生,
  • 尤其是多執行緒環境下, 導致KVO重複新增觀察者或者重複移除觀察者的情況, 這種類似的情況比較難排查.

  • 可以這樣管理混亂的KVO關係:

  • 讓觀察者物件持有一個KVO的delegate, 所有和KVO相關的操作均通過delegate來進行管理,
  • delegate通過建立一張Map表來維護KVO的整個關係, 如下圖:

image.png - 這樣做的好處如下: 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 NSObjectdealloc方法,
  • 在物件真正dealloc之前先呼叫一下[[NSNotificationCenter defaultCenter] removeObserve:self].

3.4 NSTimer記憶體洩漏造成的崩潰處理

  • 產生原因: Runloop -> NSTimer --> <- - 物件 <-VC
  • 這就導致了記憶體洩漏
  • 處理方法如下:
  • NSTimer和物件間新增一箇中間物件, NSTimer強引用中間物件, 中間物件弱引用NSTimer、物件 image.png

3.5 容器型別越界造成的崩潰處理

  • 針對NSArray、NSMutableArray、NSDictionary、NSMutableDictionary、NSCache的一些常用的, 可能會導致崩潰的API進行基於runtime的method swizzling, 然後在swizzle的新方法中針對Debug環境和Release加入一些判空處理操作, 從而讓這些API變得更難崩潰.

3.6 子執行緒重新整理UI造成的崩潰處理

  • 採用基於runtime的swizzleUIView類的重新整理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,坑位有限,備註“掘金網友”可被群管通過~