如何有效下降APP的崩潰率?

做爲一個資深的技術團隊,app的性能是咱們技術團隊首要的任務,其中最主要的一項就是app的崩潰率。數組

目前雖然不能把系統全部的crash都處理掉,不過一些常見的高頻次發生的crash,系統都會處理。目前主要能夠處理掉的crash類型有一下幾種:安全

1.unrecognized selector crashapp

2.KVO crashasync

3.NSNotification crash函數

4.NSTimer crash性能

5.Container crash(數組越界,插nil等)spa

6.NSString crash (字符串操做的crash)線程

7.UI not on Main Thread Crash (非主線程刷UI(機制待改善))指針

下面會一一講解如何解決這些carshserver

unrecognized selector crash

unrecognized selector類型的crash是常常發生的carsh,咱們要解決這個carsh就必須先了解它產生的具體緣由和流程。

何時會報unrecognized selector的異常?

objc在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類,而後在該類中的方法列表以及其父類方法列表中尋找方法運行,若是,在最頂層的父類中依然找不到相應的方法時,程序在運行時會掛掉並拋出異常unrecognized selector sent to XXX

在找不到方法時,查找方法將會進入方法Forward流程,系統給了三次補救的機會,因此咱們要解決這個問題,在這三次都可以解決這個問題

 

編輯

請點擊輸入圖片描述

由上圖可見,在一個函數找不到時,runtime提供了三種方式去補救:

一、調用resolveInstanceMethod給個機會讓類添加這個實現這個函數

二、調用forwardingTargetForSelector讓別的對象去執行這個函數

三、調用forwardInvocation(函數執行器)靈活的將目標函數以其餘形式執行。

若是都不中,調用doesNotRecognizeSelector拋出異常。既然能夠補救,咱們能夠用消息轉發機制來作,咱們選擇了第二步forwardingTargetForSelector來作,緣由以下:

一、resolveInstanceMethod 須要在類的自己上動態添加它自己不存在的方法,這些方法對於該類自己來講冗餘的

二、forwardInvocation能夠經過NSInvocation的形式將消息轉發給多個對象,可是其開銷較大,須要建立新的NSInvocation對象,而且forwardInvocation的函數常常被使用者調用,來作多層消息轉發選擇機制,不適合屢次重寫

三、forwardingTargetForSelector能夠將消息轉發給一個對象,開銷較小,而且被重寫的機率較低,適合重寫

選擇了forwardingTargetForSelector以後,能夠將NSObject的該方法重寫,作如下幾步的處理:

一、動態建立一個樁類

二、動態爲樁類添加對應的Selector,用一個通用的返回0的函數來實現該SEL的IMP

三、將消息直接轉發到這個樁類對象上。

流程圖以下:

 

編輯

請點擊輸入圖片描述

注意若是對象的類本事若是重寫了forwardInvocation方法的話,就不該該對forwardingTargetForSelector進行重寫了,不然會影響到該類型的對象本來的消息轉發流程。

經過重寫NSObject的forwardingTargetForSelector方法,咱們就能夠將沒法識別的方法進行攔截而且將消息轉發到安全的樁類對象中,從而可使app繼續正常運行。

KVO crash

若是觀察者和keypath的數量一多,很容易理不清楚被觀察對象整個KVO關係,致使被觀察者在dealloc的時候,還殘存着一些關係沒有被註銷。 同時還會致使KVO註冊觀察者與移除觀察者不匹配的狀況發生。

那麼如何來管理混亂的KVO關係呢。可讓被觀察對象持有一個KVO的delegate,全部和KVO相關的操做均經過delegate來進行管理,delegate經過創建一張map來維護KVO整個關係

這樣作的好處有兩個:

一、若是出現KVO重複添加觀察者或重複移除觀察者(KVO註冊觀察者與移除觀察者不匹配)的狀況,delegate能夠直接阻止這些非正常的操做。

二、被觀察對象dealloc以前,能夠經過delegate自動將與本身有關的KVO關係都註銷掉,避免了KVO的被觀察者dealloc時仍然註冊着KVO致使的crash。

被swizzle的方法分別是:

- (void)addObserver:(NSObject *)observer

forKeyPath:(NSString *)keyPath

options:(NSKeyValueObservingOptions)options

context:(nullable void *)context;

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

關於

- (void)addObserver:(NSObject *)observer

forKeyPath:(NSString *)keyPath

options:(NSKeyValueObservingOptions)options

context:(nullable void *)context;

方法改造流程以下圖:

 

編輯

請點擊輸入圖片描述

經過上面的流程,將observerd對象的全部kvo相關的observer信息所有轉移到KVOdelegate上,而且避免了相同kvoinfo被重複添加屢次的可能性。

關於

- (void)removeObserver:(NSObject *)observer

forKeyPath:(NSString *)keyPath

context:(void *)context

方法改造流程以下圖:

 

編輯

請點擊輸入圖片描述

移除一個keypath的Observer時,當delegate的kvoInfoMap中找不到key爲該keypath的時候,說明此時delegate並無持有對應keypath的observer,即說明移除了一個不匹配的觀察者,此時若是再繼續操做會致使app崩潰,因此應該及時中斷流程,而後統計異常信息。

當keypath對應的KVOInfo列表(infoArray)爲空的時候,說明此時delegate已經再也不持有任何和keypath相關的observer了。這時應該調用原有removeObserver的方法將delegate對應的觀察者移除。

注意到在檢查遍歷infoArray的時侯,除了要刪除對應的info信息,還多了一步檢查info.observer == nil的過程,是由於若是observer爲nil,那麼此時若是keypath對應的值變化的話,也會由於找不到observer而崩潰,因此須要作這一步來阻止該種狀況的發生。

關於

- (void)observeValueForKeyPath:(NSString *)keyPath

ofObject:(id)object

change:(NSDictionary<NSString *,id> *)change

context:(void *)context

delegate對於observeValueForKeyPath方法的修改最主要的地法規,在於將對應的響應方法轉移給真正的KVO Observer,經過keyInfoMap找到keypath對應的KVOInfo裏面預先存儲好的observer,而後調用observer本來的響應方法

同時在遍歷InfoArray的時候,發現info.observerw == nil的時候,須要及時將其清除掉,避免KVO的觀察者observer被釋放後value變化致使的crash

最後,針對 KVO的被觀察者dealloc時仍然註冊着KVO致使的crash 的狀況

能夠將NSObject的dealloc swizzle, 在object dealloc的時候自動將其對應的kvodelegate全部和kvo相關的數據清空,而後將kvodelegate也置空。避免出現KVO的被觀察者dealloc時仍然註冊着KVO而產生的crash

NSNotification crash

當一個對象添加了notification以後,若是dealloc的時候,仍然持有notification,就會出現NSNotification類型的crash。

利用method swizzling hook NSObject的dealloc函數,再對象真正dealloc以前先調用一下[[NSNotificationCenter defaultCenter] removeObserver:self] 便可。

注意到並非全部的對象都須要作以上的操做,若是一個對象歷來沒有被NSNotificationCenter 添加爲observer的話,在其dealloc以前調用removeObserver徹底是畫蛇添足。 因此咱們hook了NSNotificationCenter的addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject 函數,在其添加observer的時候,對observer動態添加標記flag。這樣在observer dealloc的時候,就能夠經過flag標記來判斷其是否有必要調用removeObserver函數了。

NSTimer crash

NSTimer存在如下問題:

• Target是強引用,內存泄漏

• 在宿主不存在的時候,清理NSTimer

解決方法: Hook NSTimer中scheduledTimerWithTimeInterval:target:selector:userInfo:repeats方法

一、當repeats爲NO時,走原始方法

二、當repeats爲YES時,新建一個對象,聲明一個target屬性爲weak類型,指向參數的target,當中間對象的target爲空時,清理NSTimer

Container crash(數組越界,插nil等)

Container 類型的crash 指的是容器類的crash

常見的有

• NSArray

• NSMutableArray

• NSDictionary

• NSMutableDictionary

• NSCache

一些常見的越界,插入nil,等錯誤操做均會致使此類crash發生

Container crash 類型的防禦方案也比較簡單,針對於NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些經常使用的會致使崩潰的API進行method swizzling,而後在swizzle的新方法中加入一些條件限制和判斷,從而讓這些API變的安全,這裏就不展開來具體描述了。

NSString crash (字符串操做的crash)

NSString/NSMutableString 類型的crash的產生緣由和防禦方案與Container crash很相像,這裏也不展開來描述了。

UI not on Main Thread Crash (非主線程刷UI)

在非主線程刷UI將會致使app運行crash,有必要對其進行處理。 目前初步的處理方案是swizzle UIView類的如下三個方法:

- (void)setNeedsLayout;

- (void)setNeedsDisplay;

- (void)setNeedsDisplayInRect:(CGRect)rect;

在這三個方法調用的時候判斷一下當前的線程,若是不是主線程的話,直接利用

dispatch_async(dispatch_get_main_queue(), ^{

//調用本來方法

});

來將對應的刷UI的操做轉移到主線程上,同時統計錯誤信息

相關文章
相關標籤/搜索