iOS拓展---常見crash以及解決方案
[轉載]iOS常見crash以及解決方案
APP運行時Crash自動修復+捕獲系統 的設計初衷,就是為了降低app的crash率。利用Objective-C語言的動態特性,采用AOP(Aspect Oriented Programming) 面向切面編程的設計思想,做到無痕植入。能夠自動在app運行時實時捕獲導致app崩潰的破環因子,然後通過特定的技術手段去化解這些破壞因子,使app免於崩潰,照樣可以繼續正常運行,為app的持續運轉保駕護航。當然我們不可能強大到把所有類型的crash都處理掉,但是我們會對一些高頻的crash進行一一的處理,我們的目的就是降低crash率
我們常見的crash有哪些呢?
- unrecognized selector crash (沒找到對應的函數)
- KVO crash :(KVO的被觀察者dealloc時仍然註冊著KVO導致的crash,添加KVO重復添加觀察者或重復移除觀察者 )
- NSNotification crash:(當一個對象添加了notification之後,如果dealloc的時候,仍然持有notification)
- NSTimer類型crash:(需要在合適的時機invalidate 定時器,否則就會由於定時器timer強引用target的關系導致 target不能被釋放,造成內存泄露,甚至在定時任務觸發時導致crash)
- Container類型crash:(數組,字典,常見的越界,插入,nil)
- 野指針類型的crash
- 非主線程刷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以及解決方案