1. 程式人生 > >iOS拓展---常見crash以及解決方案

iOS拓展---常見crash以及解決方案

插入 des 將不 建立 程序開發 解決方案 spa 標記 def

[轉載]iOS常見crash以及解決方案

APP運行時Crash自動修復+捕獲系統 的設計初衷,就是為了降低app的crash率。利用Objective-C語言的動態特性,采用AOP(Aspect Oriented Programming) 面向切面編程的設計思想,做到無痕植入。能夠自動在app運行時實時捕獲導致app崩潰的破環因子,然後通過特定的技術手段去化解這些破壞因子,使app免於崩潰,照樣可以繼續正常運行,為app的持續運轉保駕護航。當然我們不可能強大到把所有類型的crash都處理掉,但是我們會對一些高頻的crash進行一一的處理,我們的目的就是降低crash率

我們常見的crash有哪些呢?

  1. unrecognized selector crash (沒找到對應的函數)
  2. KVO crash :(KVO的被觀察者dealloc時仍然註冊著KVO導致的crash,添加KVO重復添加觀察者或重復移除觀察者 )
  3. NSNotification crash:(當一個對象添加了notification之後,如果dealloc的時候,仍然持有notification)
  4. NSTimer類型crash:(需要在合適的時機invalidate 定時器,否則就會由於定時器timer強引用target的關系導致 target不能被釋放,造成內存泄露,甚至在定時任務觸發時導致crash)
  5. Container類型crash:(數組,字典,常見的越界,插入,nil)
  6. 野指針類型的crash
  7. 非主線程刷UI類型:(在非主線程刷UI將會導致app運行crash)……


問題和解決

一:Unrecognized Selector類型crash防護

unrecognized selector類型的crash在app眾多的crash類型中占著比較大的成分,通常是因為一個對象調用了一個不屬於它方法的方法導致的。

二:KVO類型crash防護(NSNotification)

kVO crash 產生的原因:大致有2種

第一種:KVO的被觀察者dealloc時仍然註冊著KVO導致的crash

第二種:添加KVO重復添加觀察者或重復移除觀察者(KVO註冊觀察者與移除觀察者不匹配)導致的crash


一個被觀察的對象上有若幹個觀察者,每個觀察者又有若幹條keypath.

如果觀察者和keypath的數量一多,很容易不清楚被觀察的對象整個KVO關系,導致被觀察者在dealloc的時候,

仍然殘存著一些關系沒有被註銷,同時還會導致KVO註冊者和移除觀察者不匹配的情況發生

尤其是多線程的情況下,導致KVO重復添加觀察者或者移除觀察者的情況,這種類似的情況通常發生的比較隱蔽,很難從代碼的層面上排查

KVO crash 防護方案

如何管理混亂的KVO關系呢:

可以讓觀察對象持有一個KVO的delegate,所有和KVO相關的操作均通過delegate來進行管理,delegate通過

建立一張MAP表來維護KVO的整個關系,如下圖:

技術分享圖片

這樣做的好處有2個:

1:如果出現KVO重復添加觀察或者移除觀察者(KVO註冊者不匹配的)情況,delegate,可以直接阻止這些非正常的操作。

2:被觀察對象dealloc之前,可以通過delegate自動將與自己有關的KVO關系都註銷掉,避免了KVO的被觀察者dealloc時仍然註冊著KVO導致的crash

具體實現:見demo

三:NSNotification類型crash防護(NSNotification)

3.1 NSNotification crash 產生原因:

當一個對象添加了notification之後,如果dealloc的時候,仍然持有notification,就會出現NSNotification類型的crash

NSNotification類型的crash多產生於程序員寫代碼時候犯疏忽,在NSNotificationCenter添加一個對象為observer之後,忘記了在對象dealloc的時候移除它。

所幸的是,蘋果在iOS9之後專門針對於這種情況做了處理,所以在iOS9之後,即使開發者沒有移除observer,Notification crash也不會再產生了。

不過針對於iOS9之前的用戶,我們還是有必要做一下NSNotification Crash的防護的。

NSNotification Crash的防護原理很簡單, 利用method swizzling hook NSObject的dealloc函數,再對象真正dealloc之前先調用一下

[[NSNotificationCenter defaultCenter] removeObserver:self],即可。

註意到並不是所有的對象都需要做以上的操作,如果一個對象從來沒有被NSNotificationCenter 添加為observer的話,在其dealloc之前調用removeObserver完全是多此一舉

具體實現:見demo

四:NSTimer類型crash防護(NSTimer)

4.1 NSTimer crash 產生原因

在程序開發過程中,大家會經常使用定時任務,但使用NSTimer的 scheduledTimerWithTimeInterval:target:selector:

userInfo:repeats: 接口做重復性的定時任務時存在一個問題:NSTimer會 強引用 target實例,所以需要在合適的時機invalidate 定時器,否則就會由於定時器timer強引用target的關系導致 target不能被釋放,造成內存泄露,甚至在定時任務觸發時導致crash。 crash的展現形式和具體的target執行的selector有關。

與此同時,如果NSTimer是無限重復的執行一個任務的話,也有可能導致target的selector一直被重復調用且處於無效狀態,對app的CPU,內存等性能方面均是沒有必要的浪費。所以,很有必要設計出一種方案,可以有效的防護NSTimer的濫用問題。

4.2 NSTimer crash 防護方案

上面的分析可見,NSTimer所產生的問題的主要原因是因為其沒有再一個合適的時機invalidate,同時還有NSTimer對target的強引用導致的內存泄漏問題。

那麽解決NSTimer的問題的關鍵點在於以下兩點:

  >1.NSTimer對其target是否可以不強引用

  >2.是否找到一個合適的時機,在確定NSTimer已經失效的情況下,讓NSTimer自動invalidate

關於第一個問題,target的強引用問題。 可以用如下圖的方案來解決:

技術分享圖片

在NSTimer和target之間加入一層stubTarget,stubTarget主要做為一個橋接層,負責NSTimer和target之間的通信。

同時NSTimer強引用stubTarget,而stubTarget弱引用target,這樣target和NSTimer之間的關系也就是弱引用了,意味著target可以自由的釋放,從而解決了循環引用的問題。

上文提到了stubTarget負責NSTimer和target的通信,其具體的實現過程又細分為兩大步:

step 1. swizzle NSTimer中scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 相關的方法,在新方法中動態創建stubTarget對象,stubTarget對象弱引用持有原有的target,selector,timer,targetClass等properties。然後將原target分發stubTarget上,selector回調函數為stubTarget的fireProxyTimer

step 2. 通過stubTarget的fireProxyTimer:來具體處理回調函數selector的處理和分發

當NSTimer的回調函數fireProxyTimer:被執行的時候,會自動判斷原target是否已經被釋放,如果釋放了,意味著NSTimer已經無效,此時如果還繼續調用原有target的selector很有可能會導致crash,而且是沒有必要的。所以此時需要將NSTimer invalidate,然後統計上報錯誤數據。如此一來就做到了NSTimer在合適的時機自動invalidate

補充:眾所周知,NSObject類是Objective-C中大部分類的基類。但不是很多人知道除了NSObject之外的另一個基類——NSProxy
NSProxy是一個虛類,你可以通過繼承它,並重寫這兩個方法以實現消息轉發到另一個實例

技術分享圖片

栗子:

/**
 橋接層
 NSTimer強引用WOCPWeakProxy, WOCPWeakProxy弱引用target
 這樣target和NSTimer之間的關系也就是弱引用了,意味著target可以自由的釋放,從而解決了循環引用的問題
 */
@interface WOCPWeakProxy: NSProxy
@property (nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation WOCPWeakProxy

- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

+ (instancetype)proxyWithTarget:(id)target {
    return [[WOCPWeakProxy alloc] initWithTarget:target];
}

//當不能識別方法時候,就會調用這個方法,在這個方法中,我們可以將不能識別的傳遞給其它對象處理
//由於這裏對所有的不能處理的都傳遞給_target了,所以methodSignatureForSelector和forwardInvocation不可能被執行的,所以不用再重載了吧
//其實還是需要重載methodSignatureForSelector和forwardInvocation的,為什麽呢?因為_target是弱引用的,所以當_target可能釋放了,當它被釋放了的情況下,那麽
forwardingTargetForSelector就是返回nil了.然後methodSignatureForSelector和forwardInvocation沒實現的話,就直接crash了!!! //這也是為什麽這兩個方法中隨便寫的!!! // 轉發目標選擇器 - (id)forwardingTargetForSelector:(SEL)selector { return _target; } // 函數執行器 - (void)forwardInvocation:(NSInvocation *)invocation { void *null = NULL; [invocation setReturnValue:&null]; } // 方法簽名的選擇器 - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [NSObject instanceMethodSignatureForSelector:@selector(init)]; }

具體實現:見DEMO

五:Container類型crash防護(Container)

5.1 Container crash 產生原因

Container 類型的crash 指的是容器類的crash,常見的有NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的crash。 一些常見的越界,插入nil,等錯誤操作均會導致此類crash發生。由於產生的原因比較簡單,就不展開來描述了。

該類crash雖然比較容易排查,但是其在app crash概率總比還是挺高,所以有必要對其進行防護

5.2 Container crash 防護方案

Container crash 類型的防護方案也比較簡單,針對於NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/

NSCache的一些常用的會導致崩潰的API進行method swizzling,然後在swizzle的新方法中加入一些條件限制和判斷,

從而讓這些API變的安全,這裏就不展開來具體描述了。

具體實現見DEMO

六:野指針類型的crash

6.1:野指針產生的原因

在App的所有Crash中,訪問野指針導致的Crash占了很大一部分,野指針類型crash的表現為:Exception Type:SIGSEGV,Exception Codes: SEGV_ACCERR

解決野指針導致的crash往往是一件棘手的事情,一來產生crash 的場景不好復現,二來crash之後console的信息提供的幫助有限。

XCode本身為了便於開放調試時發現野指針問題,提供了Zombie機制,能夠在發生野指針時提示出現野指針的類,

從而解決了開發階段出現野指針的問題。然而針對於線上產生的野指針問題,依舊沒有一個比較好的辦法來定位問題。

所以,因為野指針出現概率高而且難定位問題,非常有必要針對於野指針專門做一層防護措施

6.2 野指針crash 防護方案

其實網上提出的方法都不完美,而且相當復雜。網上大多是在類init初始化的時候做一個標記,然後再dealloc再做一次標記,通過2次的標記來判斷是否有內存,對於UIView UIImageview常用的類來講多次分配釋放內存消耗還是比較大的,並不是完美的解決方案

這裏教大家一個小技巧:

大家知道怎麽判斷一個實例的內存是否已經釋放了嗎?這個方法是我發現的,親測,非常有效,用於判斷當前指針的內存是否還在

if(!malloc_zone_from_ptr((__bridge const void *)(strongself)))return;

但是它也不能解決全部的問題

因為我們不知道什麽時候去調用類函數什麽時候調用屬性

這裏大家有什麽更好的想法,歡迎發表

七:非主線程刷UI類型crash防護(UI not on Main Thread)

目前初步的處理方案是swizzle UIView類的以下三個方法:

-(void)setNeedsLayout;

-(void)setNeedsDisplay;

-(void)setNeedsDisplayInRect:(CGRect)rect;

在這三個方法調用的時候判斷一下當前的線程,如果不是主線程的話,直接利用 dispatch_async(dispatch_get_main_queue(), ^{ //調用原本方法 });

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

但是真正實施了之後,發現這三個方法並不能完全覆蓋UIView相關的所有刷UI到操作,但是如果要將全部到UIView的刷UI的方法統計起來並且swizzle,感覺略笨拙而且不高效。 但是這種crash占比並不高,我們重要的宗旨是降低再降低crash率,不是徹底的完全消滅,而且我們目前也沒有辦法完全消滅,只有我們掌握了底層的原理,才能靈活應變處理問題!

iOS拓展---常見crash以及解決方案