黑魔法教你讓iOS APP防住Crash

大白健康系統(tǒng)--iOS APP運(yùn)行時(shí)Crash自動(dòng)修復(fù)系統(tǒng)

前言


大白(Baymax),迪士尼動(dòng)畫(huà)《超能陸戰(zhàn)隊(duì)》中的健康機(jī)器人拾枣,是一個(gè)體型胖胖的充氣機(jī)器人互例,因呆萌的外表和善良的本質(zhì)獲得大家的喜愛(ài)敷钾,被稱為“萌神”。

Baymax項(xiàng)目是為了減少開(kāi)發(fā)人員在開(kāi)發(fā)中一些不規(guī)范的代碼編寫(xiě)造成的內(nèi)存泄露娱两,界面卡頓,耗電等問(wèn)題而來(lái)的一個(gè)監(jiān)控系統(tǒng)金吗。

現(xiàn)在Baymax迎來(lái)了它新的功能:APP運(yùn)行時(shí)Crash自動(dòng)防護(hù)功能十兢,為app的流程順利運(yùn)行保駕護(hù)航!

下面將詳細(xì)介紹一下<APP運(yùn)行時(shí)Crash自動(dòng)修復(fù)系統(tǒng)>開(kāi)發(fā)的目的摇庙,設(shè)計(jì)的原理以及使用的方法旱物。


APP運(yùn)行時(shí)Crash自動(dòng)修復(fù)系統(tǒng)

Chapter 1 - 開(kāi)發(fā)目的

是否存在這樣的夜晚,當(dāng)剛剛躺下準(zhǔn)備美美的睡一覺(jué)的時(shí)候跟匆, 突然來(lái)一記奪命電話Call异袄,一接起來(lái)發(fā)現(xiàn)是你老板!B瓯邸烤蜕!“小王啊,剛剛上線的X.X.X版本出問(wèn)題了啊迹冤,怎么怎么樣操作會(huì)crash啊讽营,導(dǎo)致新功能都無(wú)法使用了,快定位一下是什么原因泡徙,抓緊hotpatch修復(fù)一下俺髋簟!”。心里一萬(wàn)頭草泥馬呼嘯而過(guò)莉兰,瞬間已經(jīng)滿頭大汗的你卻還要故作鎮(zhèn)靜地回答:“嗯挑围,老板我馬上去看看,一定努力解決問(wèn)題糖荒!” 急忙打開(kāi)電腦的你杉辙,知道今夜注定無(wú)眠了。

是否又存在這樣的情形捶朵,你老板把大家都聚起來(lái)開(kāi)了一個(gè)年初KPI目標(biāo)制定會(huì)議蜘矢,說(shuō)到:“作為一個(gè)資深的技術(shù)團(tuán)隊(duì),app性能是我們技術(shù)團(tuán)隊(duì)首抓的目標(biāo)综看,其中很最要的一項(xiàng)就是app的崩潰率品腹,去年我們app統(tǒng)計(jì)出來(lái)的崩潰率是千分之五,而我們的競(jìng)爭(zhēng)對(duì)手的崩潰率只有萬(wàn)分之五红碑,相差了10倍舞吭!今年我們要趕超他們,最起碼也要和他們持平句喷×偷洌” 你甚是贊同,但是你心里卻又有點(diǎn)懷疑唾琼,對(duì)方的開(kāi)發(fā)資源是我們的好幾倍而且個(gè)個(gè)都是資深老司機(jī)兄春,我們團(tuán)隊(duì)里卻大多都是應(yīng)屆生小鮮肉,這KPI能完成么锡溯?

如果你遇到過(guò)以上的情況并且對(duì)此深表頭痛的話赶舆,那么 <大白健康系統(tǒng)--APP運(yùn)行時(shí)Crash自動(dòng)修復(fù)系統(tǒng)> 將會(huì)是你的不二選擇!

APP運(yùn)行時(shí)Crash自動(dòng)修復(fù)+捕獲系統(tǒng) 的設(shè)計(jì)初衷祭饭,就是為了降低app的crash率芜茵。利用Objective-C語(yǔ)言的動(dòng)態(tài)特性,采用AOP(Aspect Oriented Programming) 面向切面編程的設(shè)計(jì)思想倡蝙,做到無(wú)痕植入九串。能夠自動(dòng)在app運(yùn)行時(shí)實(shí)時(shí)捕獲導(dǎo)致app崩潰的破環(huán)因子,然后通過(guò)特定的技術(shù)手段去化解這些破壞因子寺鸥,使app免于崩潰猪钮,照樣可以繼續(xù)正常運(yùn)行,為app的持續(xù)運(yùn)轉(zhuǎn)保駕護(hù)航胆建。


Chapter 2 - 功能簡(jiǎn)介

APP運(yùn)行時(shí)Crash自動(dòng)修復(fù)系統(tǒng)的主要功能烤低,可以用一句話來(lái)簡(jiǎn)單的概括:對(duì)業(yè)務(wù)代碼的零侵入性地將原本會(huì)導(dǎo)致app崩潰的crash抓取住,消滅掉笆载,保證app繼續(xù)正常地運(yùn)行扑馁,再將crash的具體信息提取出來(lái)涯呻,實(shí)時(shí)返回給用戶

通過(guò)下面的一個(gè)小例子就可以很直觀的體現(xiàn)出來(lái)系統(tǒng)的作用:

調(diào)用以下的一段代碼

//test code

UIButton * testObj = [[UIButton alloc] init];

[testObj performSelector:@selector(someMethod:)];

結(jié)果肯定會(huì)導(dǎo)致app的崩潰腻要,因?yàn)閠estObj是一個(gè)UIButton對(duì)象复罐,而UIButton并沒(méi)有實(shí)現(xiàn) someMethod: 這個(gè)方法,所以向testObj發(fā)送someMethod:這個(gè)方法的時(shí)候雄家,將會(huì)導(dǎo)致該方法無(wú)法在相關(guān)的方法列表里找到市栗,最終導(dǎo)致app的crash。

但是通過(guò)我們的crash防護(hù)系統(tǒng)咳短,調(diào)用這段代碼時(shí)app并不會(huì)崩潰,同時(shí)XCode的Console如下:


image

可見(jiàn)對(duì)應(yīng)的crash的信息(crash類型蛛淋,原因咙好,調(diào)用棧信息)均可以完整的打印在XCode的Console中。

說(shuō)明我們的大白系統(tǒng)已經(jīng)捕捉到了這個(gè)crash褐荷,將該crash消滅掉并且吐出來(lái)該crash的完整信息勾效。

當(dāng)然目前系統(tǒng)的功能并沒(méi)有強(qiáng)大到可以把所有的crash都處理掉,不過(guò)一些常見(jiàn)的高頻次發(fā)生的crash叛甫,系統(tǒng)均會(huì)針對(duì)他們一一處理层宫。目前可以處理掉的crash類型具體有以下幾種:

  • unrecognized selector crash
  • KVO crash
  • NSNotification crash
  • NSTimer crash
  • Container crash(數(shù)組越界,插nil等)
  • NSString crash (字符串操作的crash)
  • UI not on Main Thread Crash (非主線程刷UI(機(jī)制待改善))

對(duì)于每種類型的crash其监,安全系統(tǒng)都采取不同的方式萌腿,進(jìn)行了對(duì)應(yīng)的處理。 具體的處理細(xì)節(jié)詳見(jiàn)下章:Chapter 3 - 實(shí)現(xiàn)原理


Chapter 3 - 實(shí)現(xiàn)原理

前面已經(jīng)提過(guò)抖苦,目前的安全防護(hù)系統(tǒng)可以覆蓋到8中類型的Crash毁菱,分別為:

接下來(lái)將一一詳細(xì)介紹這8種類型的Crash的防護(hù)的實(shí)現(xiàn)的具體原理:

3.1 Unrecognized Selector類型crash防護(hù)(Unrecognized Selector)

3.1.1 unrecognized selector crash 產(chǎn)生原因

unrecognized selector類型的crash在app眾多的crash類型中占著比較大的成分锌历,通常是因?yàn)橐粋€(gè)對(duì)象調(diào)用了一個(gè)不屬于它方法的方法導(dǎo)致的贮庞。

例如調(diào)用以下一段代碼就會(huì)產(chǎn)生crash

//test code

UIButton * testObj = [[UIButton alloc] init];

[testObj performSelector:@selector(someMethod:)];

具體crash時(shí)的表現(xiàn)見(jiàn)下圖:

image

要解決這中類型的crash,我們需要先了解清楚它產(chǎn)生的具體原因和流程究西。

3.1.2 方法調(diào)用流程

讓我們看一下方法調(diào)用在運(yùn)行時(shí)的過(guò)程窗慎。

runtime中具體的方法調(diào)用流程大致如下:

1.首先,在相應(yīng)操作的對(duì)象中的緩存方法列表中找調(diào)用的方法卤材,如果找到狸窘,轉(zhuǎn)向相應(yīng)實(shí)現(xiàn)并執(zhí)行。

2.如果沒(méi)找到提鸟,在相應(yīng)操作的對(duì)象中的方法列表中找調(diào)用的方法帕翻,如果找到,轉(zhuǎn)向相應(yīng)實(shí)現(xiàn)執(zhí)行

3.如果沒(méi)找到晕拆,去父類指針?biāo)赶虻膶?duì)象中執(zhí)行1藐翎,2.

4.以此類推材蹬,如果一直到根類還沒(méi)找到,轉(zhuǎn)向攔截調(diào)用吝镣,走消息轉(zhuǎn)發(fā)機(jī)制堤器。

5.如果沒(méi)有重寫(xiě)攔截調(diào)用的方法,程序報(bào)錯(cuò)末贾。

3.1.3 攔截調(diào)用

在方法調(diào)用中說(shuō)到了闸溃,如果沒(méi)有找到方法就會(huì)轉(zhuǎn)向攔截調(diào)用。

那么什么是攔截調(diào)用呢拱撵。

攔截調(diào)用就是辉川,在找不到調(diào)用的方法程序崩潰之前,你有機(jī)會(huì)通過(guò)重寫(xiě)NSObject的四個(gè)方法來(lái)處理:

+ (BOOL)resolveClassMethod:(SEL)sel;

+ (BOOL)resolveInstanceMethod:(SEL)sel;

//后兩個(gè)方法需要轉(zhuǎn)發(fā)到其他的類處理

- (id)forwardingTargetForSelector:(SEL)aSelector;

- (void)forwardInvocation:(NSInvocation *)anInvocation;

攔截調(diào)用的整個(gè)流程即Objective——C的消息轉(zhuǎn)發(fā)機(jī)制拴测。其具體流程如下圖:

image

由上圖可見(jiàn)乓旗,在一個(gè)函數(shù)找不到時(shí),runtime提供了三種方式去補(bǔ)救:

1集索、調(diào)用resolveInstanceMethod給個(gè)機(jī)會(huì)讓類添加這個(gè)實(shí)現(xiàn)這個(gè)函數(shù)

2屿愚、調(diào)用forwardingTargetForSelector讓別的對(duì)象去執(zhí)行這個(gè)函數(shù)

3、調(diào)用forwardInvocation(函數(shù)執(zhí)行器)靈活的將目標(biāo)函數(shù)以其他形式執(zhí)行务荆。

如果都不中妆距,調(diào)用doesNotRecognizeSelector拋出異常。

3.1.4 unrecognized selector crash 防護(hù)方案

既然可以補(bǔ)救函匕,我們完全也可以利用消息轉(zhuǎn)發(fā)機(jī)制來(lái)做文章娱据。那么問(wèn)題來(lái)了,在這三個(gè)步驟里面盅惜,選擇哪一步去改造比較合適呢吸耿。

這里我們選擇了第二步forwardingTargetForSelector來(lái)做文章。原因如下:

  1. resolveInstanceMethod 需要在類的本身上動(dòng)態(tài)添加它本身不存在的方法酷窥,這些方法對(duì)于該類本身來(lái)說(shuō)冗余的
  2. forwardInvocation可以通過(guò)NSInvocation的形式將消息轉(zhuǎn)發(fā)給多個(gè)對(duì)象咽安,但是其開(kāi)銷較大,需要?jiǎng)?chuàng)建新的NSInvocation對(duì)象蓬推,并且forwardInvocation的函數(shù)經(jīng)常被使用者調(diào)用妆棒,來(lái)做多層消息轉(zhuǎn)發(fā)選擇機(jī)制,不適合多次重寫(xiě)
  3. forwardingTargetForSelector可以將消息轉(zhuǎn)發(fā)給一個(gè)對(duì)象沸伏,開(kāi)銷較小糕珊,并且被重寫(xiě)的概率較低,適合重寫(xiě)

選擇了forwardingTargetForSelector之后毅糟,可以將NSObject的該方法重寫(xiě)红选,做以下幾步的處理:

1.動(dòng)態(tài)創(chuàng)建一個(gè)樁類

2.動(dòng)態(tài)為樁類添加對(duì)應(yīng)的Selector,用一個(gè)通用的返回0的函數(shù)來(lái)實(shí)現(xiàn)該SEL的IMP

3.將消息直接轉(zhuǎn)發(fā)到這個(gè)樁類對(duì)象上姆另。

流程圖如下:

image

注意如果對(duì)象的類本事如果重寫(xiě)了forwardInvocation方法的話喇肋,就不應(yīng)該對(duì)forwardingTargetForSelector進(jìn)行重寫(xiě)了坟乾,否則會(huì)影響到該類型的對(duì)象原本的消息轉(zhuǎn)發(fā)流程。

通過(guò)重寫(xiě)NSObject的forwardingTargetForSelector方法蝶防,我們就可以將無(wú)法識(shí)別的方法進(jìn)行攔截并且將消息轉(zhuǎn)發(fā)到安全的樁類對(duì)象中甚侣,從而可以使app繼續(xù)正常運(yùn)行。

3.2 KVO類型crash防護(hù)(KVO)

3.2.1 KVO crash 產(chǎn)生原因

KVO,即:Key-Value Observing间学,它提供一種機(jī)制殷费,當(dāng)指定的對(duì)象的屬性被修改后,則對(duì)象就會(huì)接受收到通知低葫。簡(jiǎn)單的說(shuō)就是每次指定的被觀察的對(duì)象的屬性被修改后详羡,KVO就會(huì)自動(dòng)通知相應(yīng)的觀察者了。

KVO機(jī)制在iOS的很多開(kāi)發(fā)場(chǎng)景中都會(huì)被使用到嘿悬。不過(guò)如果一不小心使用不當(dāng)?shù)脑捯笊埽瑫?huì)導(dǎo)致大量的crash問(wèn)題。所以如果能找到一種方法能夠自動(dòng)抓取這些由于開(kāi)發(fā)者粗心所導(dǎo)致的KVO Crash問(wèn)題的話鹊漠,是有一定的價(jià)值的。

首先我們來(lái)看看通過(guò)會(huì)導(dǎo)致KVO Crash的兩種情形:

  1. KVO的被觀察者dealloc時(shí)仍然注冊(cè)著KVO導(dǎo)致的crash茶行,見(jiàn)下圖
image
  1. 添加KVO重復(fù)添加觀察者或重復(fù)移除觀察者(KVO注冊(cè)觀察者與移除觀察者不匹配)導(dǎo)致的crash躯概,見(jiàn)下圖
image

3.2.2 KVO crash 防護(hù)方案

通常一個(gè)對(duì)象的KVO關(guān)系圖如下:

image

一個(gè)被觀察的對(duì)象(Observed Object)上有若干個(gè)觀察者(Observer),每個(gè)觀察者又觀察若干條KeyPath。

如果觀察者和keypath的數(shù)量一多畔师,很容易理不清楚被觀察對(duì)象整個(gè)KVO關(guān)系娶靡,導(dǎo)致被觀察者在dealloc的時(shí)候,還殘存著一些關(guān)系沒(méi)有被注銷看锉。 同時(shí)還會(huì)導(dǎo)致KVO注冊(cè)觀察者與移除觀察者不匹配的情況發(fā)生姿锭。

筆者曾經(jīng)還遇到過(guò)在多線程的情況下,導(dǎo)致KVO重復(fù)添加觀察者或移除觀察者的情況伯铣。這類問(wèn)題通常多數(shù)發(fā)生的比較隱蔽呻此,不容易從代碼的層面去排查。

由上可見(jiàn)多數(shù)由于KVO而導(dǎo)致的crash原因是由于被觀察對(duì)象的KVO關(guān)系圖混亂導(dǎo)致腔寡。那么如何來(lái)管理混亂的KVO關(guān)系呢焚鲜。可以讓被觀察對(duì)象持有一個(gè)KVO的delegate放前,所有和KVO相關(guān)的操作均通過(guò)delegate來(lái)進(jìn)行管理忿磅,delegate通過(guò)建立一張map來(lái)維護(hù)KVO整個(gè)關(guān)系。如下圖:

image

這樣做的好處有兩個(gè):

1.如果出現(xiàn)KVO重復(fù)添加觀察者或重復(fù)移除觀察者(KVO注冊(cè)觀察者與移除觀察者不匹配)的情況凭语,delegate可以直接阻止這些非正常的操作葱她。

2.被觀察對(duì)象dealloc之前,可以通過(guò)delegate自動(dòng)將與自己有關(guān)的KVO關(guān)系都注銷掉似扔,避免了KVO的被觀察者dealloc時(shí)仍然注冊(cè)著KVO導(dǎo)致的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;

關(guān)于

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context

方法改造流程如下圖:

image

通過(guò)上面的流程搓谆,將observerd對(duì)象的所有kvo相關(guān)的observer信息全部轉(zhuǎn)移到KVOdelegate上,并且避免了相同kvoinfo被重復(fù)添加多次的可能性锤灿。

關(guān)于

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

方法改造流程如下圖:

image

移除一個(gè)keypath的Observer時(shí)挽拔,當(dāng)delegate的kvoInfoMap中找不到key為該keypath的時(shí)候,說(shuō)明此時(shí)delegate并沒(méi)有持有對(duì)應(yīng)keypath的observer但校,即說(shuō)明移除了一個(gè)不匹配的觀察者螃诅,此時(shí)如果再繼續(xù)操作會(huì)導(dǎo)致app崩潰,所以應(yīng)該及時(shí)中斷流程状囱,然后統(tǒng)計(jì)異常信息术裸。

當(dāng)keypath對(duì)應(yīng)的KVOInfo列表(infoArray)為空的時(shí)候,說(shuō)明此時(shí)delegate已經(jīng)不再持有任何和keypath相關(guān)的observer了亭枷。這時(shí)應(yīng)該調(diào)用原有removeObserver的方法將delegate對(duì)應(yīng)的觀察者移除袭艺。

注意到在檢查遍歷infoArray的時(shí)侯,除了要?jiǎng)h除對(duì)應(yīng)的info信息叨粘,還多了一步檢查info.observer == nil的過(guò)程猾编,是因?yàn)槿绻鹢bserver為nil,那么此時(shí)如果keypath對(duì)應(yīng)的值變化的話升敲,也會(huì)因?yàn)檎也坏給bserver而崩潰答倡,所以需要做這一步來(lái)阻止該種情況的發(fā)生。

關(guān)于

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

方法改造流程如下圖:

image

delegate對(duì)于observeValueForKeyPath方法的修改最主要的地法規(guī)驴党,在于將對(duì)應(yīng)的響應(yīng)方法轉(zhuǎn)移給真正的KVO Observer瘪撇,通過(guò)keyInfoMap找到keypath對(duì)應(yīng)的KVOInfo里面預(yù)先存儲(chǔ)好的observer,然后調(diào)用observer原本的響應(yīng)方法

同時(shí)在遍歷InfoArray的時(shí)候港庄,發(fā)現(xiàn)info.observerw == nil的時(shí)候倔既,需要及時(shí)將其清除掉,避免KVO的觀察者observer被釋放后value變化導(dǎo)致的crash

最后鹏氧,針對(duì) KVO的被觀察者dealloc時(shí)仍然注冊(cè)著KVO導(dǎo)致的crash 的情況

可以將NSObject的dealloc swizzle渤涌, 在object dealloc的時(shí)候自動(dòng)將其對(duì)應(yīng)的kvodelegate所有和kvo相關(guān)的數(shù)據(jù)清空,然后將kvodelegate也置空把还。避免出現(xiàn)KVO的被觀察者dealloc時(shí)仍然注冊(cè)著KVO而產(chǎn)生的crash

3.3 NSNotification類型crash防護(hù)(NSNotification)

3.3.1 NSNotification crash 產(chǎn)生原因

當(dāng)一個(gè)對(duì)象添加了notification之后歼捏,如果dealloc的時(shí)候,仍然持有notification笨篷,就會(huì)出現(xiàn)NSNotification類型的crash瞳秽。

NSNotification類型的crash多產(chǎn)生于程序員寫(xiě)代碼時(shí)候犯疏忽,在NSNotificationCenter添加一個(gè)對(duì)象為observer之后率翅,忘記了在對(duì)象dealloc的時(shí)候移除它练俐。

所幸的是,蘋(píng)果在iOS9之后專門(mén)針對(duì)于這種情況做了處理冕臭,所以在iOS9之后腺晾,即使開(kāi)發(fā)者沒(méi)有移除observer燕锥,Notification crash也不會(huì)再產(chǎn)生了。

不過(guò)針對(duì)于iOS9之前的用戶悯蝉,我們還是有必要做一下NSNotification Crash的防護(hù)的归形。

3.3.2 NSNotification crash 防護(hù)方案

NSNotification Crash的防護(hù)原理很簡(jiǎn)單, 利用method swizzling hook NSObject的dealloc函數(shù)鼻由,再對(duì)象真正dealloc之前先調(diào)用一下
[[NSNotificationCenter defaultCenter] removeObserver:self]
即可暇榴。

注意到并不是所有的對(duì)象都需要做以上的操作,如果一個(gè)對(duì)象從來(lái)沒(méi)有被NSNotificationCenter 添加為observer的話蕉世,在其dealloc之前調(diào)用removeObserver完全是多此一舉蔼紧。 所以我們hook了NSNotificationCenter的

- (void) addObserver:(id)observer 
            selector:(SEL)aSelector 
                name:(NSString *)aName 
               object:(id)anObject

函數(shù),在其添加observer的時(shí)候狠轻,對(duì)observer動(dòng)態(tài)添加標(biāo)記flag奸例。這樣在observer dealloc的時(shí)候,就可以通過(guò)flag標(biāo)記來(lái)判斷其是否有必要調(diào)用removeObserver函數(shù)了向楼。

3.4 NSTimer類型crash防護(hù)(NSTimer)

3.4.1 NSTimer crash 產(chǎn)生原因

在程序開(kāi)發(fā)過(guò)程中查吊,大家會(huì)經(jīng)常使用定時(shí)任務(wù),但使用NSTimer的 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 接口做重復(fù)性的定時(shí)任務(wù)時(shí)存在一個(gè)問(wèn)題:NSTimer會(huì) 強(qiáng)引用 target實(shí)例湖蜕,所以需要在合適的時(shí)機(jī)invalidate 定時(shí)器逻卖,否則就會(huì)由于定時(shí)器timer強(qiáng)引用target的關(guān)系導(dǎo)致 target不能被釋放,造成內(nèi)存泄露重荠,甚至在定時(shí)任務(wù)觸發(fā)時(shí)導(dǎo)致crash。 crash的展現(xiàn)形式和具體的target執(zhí)行的selector有關(guān)虚茶。

與此同時(shí)戈鲁,如果NSTimer是無(wú)限重復(fù)的執(zhí)行一個(gè)任務(wù)的話,也有可能導(dǎo)致target的selector一直被重復(fù)調(diào)用且處于無(wú)效狀態(tài)嘹叫,對(duì)app的CPU婆殿,內(nèi)存等性能方面均是沒(méi)有必要的浪費(fèi)。

所以罩扇,很有必要設(shè)計(jì)出一種方案婆芦,可以有效的防護(hù)NSTimer的濫用問(wèn)題。

3.4.2 NSTimer crash 防護(hù)方案

上面的分析可見(jiàn)喂饥,NSTimer所產(chǎn)生的問(wèn)題的主要原因是因?yàn)槠錄](méi)有再一個(gè)合適的時(shí)機(jī)invalidate消约,同時(shí)還有NSTimer對(duì)target的強(qiáng)引用導(dǎo)致的內(nèi)存泄漏問(wèn)題。

那么解決NSTimer的問(wèn)題的關(guān)鍵點(diǎn)在于以下兩點(diǎn):

  1. NSTimer對(duì)其target是否可以不強(qiáng)引用
  2. 是否找到一個(gè)合適的時(shí)機(jī)员帮,在確定NSTimer已經(jīng)失效的情況下或粮,讓NSTimer自動(dòng)invalidate

關(guān)于第一個(gè)問(wèn)題,target的強(qiáng)引用問(wèn)題捞高。 可以用如下圖的方案來(lái)解決:

image

在NSTimer和target之間加入一層stubTarget氯材,stubTarget主要做為一個(gè)橋接層渣锦,負(fù)責(zé)NSTimer和target之間的通信。

同時(shí)NSTimer強(qiáng)引用stubTarget氢哮,而stubTarget弱引用target袋毙,這樣target和NSTimer之間的關(guān)系也就是弱引用了,意味著target可以自由的釋放冗尤,從而解決了循環(huán)引用的問(wèn)題听盖。

上文提到了stubTarget負(fù)責(zé)NSTimer和target的通信,其具體的實(shí)現(xiàn)過(guò)程又細(xì)分為兩大步:

step 1. swizzle NSTimer中scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 相關(guān)的方法生闲,在新方法中動(dòng)態(tài)創(chuàng)建stubTarget對(duì)象媳溺,stubTarget對(duì)象弱引用持有原有的target,selector碍讯,timer悬蔽,targetClass等properties。然后將原target分發(fā)stubTarget上捉兴,selector回調(diào)函數(shù)為stubTarget的fireProxyTimer:蝎困,流程如下圖:

image

step 2. 通過(guò)stubTarget的fireProxyTimer:來(lái)具體處理回調(diào)函數(shù)selector的處理和分發(fā),流程如下圖:

image

因?yàn)閟tubTarget的介入倍啥,原有的target已經(jīng)可以不受NSTimer強(qiáng)引用的牽制禾乘,而自由的釋放。

由上圖流程可知虽缕,當(dāng)NSTimer的回調(diào)函數(shù)fireProxyTimer:被執(zhí)行的時(shí)候始藕,會(huì)自動(dòng)判斷原target是否已經(jīng)被釋放,如果釋放了氮趋,意味著NSTimer已經(jīng)無(wú)效伍派,此時(shí)如果還繼續(xù)調(diào)用原有target的selector很有可能會(huì)導(dǎo)致crash,而且是沒(méi)有必要的剩胁。所以此時(shí)需要將NSTimer invalidate诉植,然后統(tǒng)計(jì)上報(bào)錯(cuò)誤數(shù)據(jù)。如此一來(lái)就做到了NSTimer在合適的時(shí)機(jī)自動(dòng)invalidate昵观。

3.5 Container類型crash防護(hù)(Container)

3.5.1 Container crash 產(chǎn)生原因

Container 類型的crash 指的是容器類的crash晾腔,常見(jiàn)的有NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的crash。 一些常見(jiàn)的越界啊犬,插入nil灼擂,等錯(cuò)誤操作均會(huì)導(dǎo)致此類crash發(fā)生。 由于產(chǎn)生的原因比較簡(jiǎn)單觉至,就不展開(kāi)來(lái)描述了缤至。

該類crash雖然比較容易排查,但是其在app crash概率總比還是挺高,所以有必要對(duì)其進(jìn)行防護(hù)领斥。

3.5.2 Container crash 防護(hù)方案

Container crash 類型的防護(hù)方案也比較簡(jiǎn)單嫉到,針對(duì)于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些常用的會(huì)導(dǎo)致崩潰的API進(jìn)行method swizzling,然后在swizzle的新方法中加入一些條件限制和判斷月洛,從而讓這些API變的安全何恶,這里就不展開(kāi)來(lái)具體描述了。

3.6 NSString類型crash防護(hù)(NSString)

NSString/NSMutableString 類型的crash的產(chǎn)生原因和防護(hù)方案與Container crash很相像嚼黔,這里也不展開(kāi)來(lái)描述了细层。

3.8 非主線程刷UI類型crash防護(hù)(UI not on Main Thread)

在非主線程刷UI將會(huì)導(dǎo)致app運(yùn)行crash,有必要對(duì)其進(jìn)行處理唬涧。

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

- (void)setNeedsLayout;

- (void)setNeedsDisplay;

- (void)setNeedsDisplayInRect:(CGRect)rect;

在這三個(gè)方法調(diào)用的時(shí)候判斷一下當(dāng)前的線程疫赎,如果不是主線程的話,直接利用

dispatch_async(dispatch_get_main_queue(), ^{
            //調(diào)用原本方法
        });

來(lái)將對(duì)應(yīng)的刷UI的操作轉(zhuǎn)移到主線程上碎节,同時(shí)統(tǒng)計(jì)錯(cuò)誤信息捧搞。

但是真正實(shí)施了之后,發(fā)現(xiàn)這三個(gè)方法并不能完全覆蓋UIView相關(guān)的所有刷UI到操作狮荔,但是如果要將全部到UIView的刷UI的方法統(tǒng)計(jì)起來(lái)并且swizzle胎撇,感覺(jué)略笨拙而且不高效。

所以作者依舊在尋找殖氏,看是否有更好的方案來(lái)解決該問(wèn)題晚树。


Chapter 4 - 使用手冊(cè)

目前sdk實(shí)現(xiàn)了以下的功能和配置:

1. 配置需要防護(hù)的crash類型

可以根據(jù)自身需要,選擇一定的crash防護(hù)配置雅采,通過(guò)以下的接口進(jìn)行配置:

- (void)configSafetyGuardService:(HTSafetyGuardType)SafetyGuardType;

其中可以配置的SafetyGuardType有:

  • HTSafetyGuardType_None
  • HTSafetyGuardType_All
  • HTSafetyGuardType_UnrecognizedSelector
  • HTSafetyGuardType_KVO
  • HTSafetyGuardType_Notification
  • HTSafetyGuardType_Timer
  • HTSafetyGuardType_Container
  • HTSafetyGuardType_String
  • HTSafetyGuardType_UI

可以根據(jù)自己項(xiàng)目的需求自行選擇需要防護(hù)的類型爵憎。

2. 實(shí)時(shí) 開(kāi)啟/暫停 安全防護(hù)功能

配置完畢之后,需要調(diào)用- (void)start;來(lái)開(kāi)啟防護(hù)婚瓜,防護(hù)的開(kāi)關(guān)是實(shí)時(shí)的(無(wú)需重啟app)宝鼓,可以在任意的時(shí)刻選擇 開(kāi)啟/關(guān)閉 防護(hù)功能。

通過(guò) - (BOOL)isWorking 接口可以獲取當(dāng)前防護(hù)功能的狀態(tài)闰渔。

通過(guò) - (void)start 接口實(shí)時(shí)開(kāi)啟防護(hù)功能

通過(guò) - (void)stop 接口實(shí)時(shí)關(guān)閉防護(hù)功能

3. 配置白名單和黑名單席函,指定對(duì)應(yīng)的想 加上/去掉 安全防護(hù)功能的類和對(duì)象

由于不同類實(shí)現(xiàn)的特殊性铐望,考慮到可能某些類并不需要開(kāi)啟防護(hù)功能冈涧。 所以提供了黑名單的功能。
在黑名單里面的類本身以及其子類正蛙,都不會(huì)進(jìn)入防護(hù)的范圍督弓。

白名單的出現(xiàn)是因?yàn)樽髡咴陂_(kāi)發(fā)的時(shí)候發(fā)現(xiàn)一些系統(tǒng)自帶的類是沒(méi)有必要進(jìn)入防護(hù)范圍的,所以將整體防護(hù)的范圍調(diào)整到所有用戶自定義的類里面乒验。 但是之后又發(fā)現(xiàn)絕大多數(shù)的crash和一些常用的系統(tǒng)的類(例如NSString愚隧,NSDictionary,UIView等等)有很強(qiáng)的聯(lián)系锻全,針對(duì)于這些常用的系統(tǒng)類還是很有必要開(kāi)啟防護(hù)的狂塘。所以針對(duì)這些需要防護(hù)的系統(tǒng)類录煤,專門(mén)提供了白名單的功能。

4. 設(shè)置異常處理handler荞胡,指定出現(xiàn)crash被抓取情況之后妈踊,用戶想自定義的操作

出現(xiàn)了crash,并且被我們的系統(tǒng)捕捉到加以處理之后泪漂,用戶可能還需要進(jìn)一步的處理廊营,例如上傳埋點(diǎn)等。這時(shí)可以通過(guò)設(shè)置一個(gè)handler來(lái)實(shí)現(xiàn)萝勤, HTExceptionHandler會(huì)將crash的信息通過(guò)HTCrashInfo的形式來(lái)返回露筒。

HTCrashInfo內(nèi)包含了:

  • 導(dǎo)致crash的類型:crashType
  • crash線程的調(diào)用棧:callStackSymbols
  • crash的具體描述信息:crashDescription
  • 擴(kuò)展信息:userinfo

以上接口具體詳細(xì)的信息均可以在(HTSafetyGuardService.h)中找到。(注意HTSafetyGuardService是單例)


由于目前sdk還未經(jīng)過(guò)完整的功能測(cè)試和性能測(cè)試敌卓,故暫不開(kāi)放對(duì)應(yīng)的sdk慎式。等作者覺(jué)得項(xiàng)目質(zhì)量達(dá)到了一定的標(biāo)準(zhǔn)之后,會(huì)將項(xiàng)目sdk開(kāi)放出來(lái)假哎。如果對(duì)該項(xiàng)目感興趣瞬捕,可以聯(lián)系 taozeyu890217@126.com,歡迎一起研究舵抹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末肪虎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子惧蛹,更是在濱河造成了極大的恐慌扇救,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件香嗓,死亡現(xiàn)場(chǎng)離奇詭異迅腔,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)靠娱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)沧烈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人像云,你說(shuō)我怎么就攤上這事锌雀。” “怎么了迅诬?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵腋逆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我侈贷,道長(zhǎng)惩歉,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮撑蚌,結(jié)果婚禮上上遥,老公的妹妹穿的比我還像新娘。我一直安慰自己争涌,他們只是感情好露该,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著第煮,像睡著了一般解幼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上包警,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天撵摆,我揣著相機(jī)與錄音,去河邊找鬼害晦。 笑死特铝,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的壹瘟。 我是一名探鬼主播鲫剿,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼稻轨!你這毒婦竟也來(lái)了灵莲?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤殴俱,失蹤者是張志新(化名)和其女友劉穎政冻,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體线欲,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抡秆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年衷咽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片羡儿。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡氓润,死狀恐怖如筛,靈堂內(nèi)的尸體忽然破棺而出倒戏,到底是詐尸還是另有隱情煤伟,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布踱讨,位于F島的核電站魏蔗,受9級(jí)特大地震影響砍的,放射性物質(zhì)發(fā)生泄漏痹筛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望帚稠。 院中可真熱鬧谣旁,春花似錦、人聲如沸滋早。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)杆麸。三九已至搁进,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間昔头,已是汗流浹背饼问。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留揭斧,地道東北人莱革。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像讹开,于是被迫代替她去往敵國(guó)和親盅视。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容