網(wǎng)易iOS App運行時Crash自動防護實踐

Baymax:網(wǎng)易iOS App運行時Crash自動防護實踐

image

<section style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; color: rgb(63, 63, 63); font-size: 14px; font-family: Avenir, -apple-system-font, 微軟雅黑, sans-serif; text-align: center;">

版權聲明

本文轉自網(wǎng)易杭州前端技術部公眾號,由作者授權發(fā)布垄懂。

<section class="" style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 20px;">

前言

</section>

大白(Baymax)碴巾,迪士尼動畫《超能陸戰(zhàn)隊》中的健康機器人苏章,是一個體型胖胖的充氣機器人酿雪,因呆萌的外表和善良的本質獲得大家的喜愛,被稱為“萌神”贡耽。

Baymax項目是為了減少開發(fā)人員在開發(fā)中一些不規(guī)范的代碼編寫造成的內(nèi)存泄露衷模,界面卡頓鹊汛,耗電等問題而來的一個監(jiān)控系統(tǒng)。

現(xiàn)在Baymax迎來了它新的功能:APP運行時Crash自動防護功能阱冶,為app的流程順利運行保駕護航刁憋!

下面將詳細介紹一下APP運行時Crash自動修復系統(tǒng)開發(fā)的目的,設計的原理以及使用的方法木蹬。

<section class="" style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px;">

Chapter 1 - 開發(fā)目的

</section>

是否存在這樣的夜晚至耻,當剛剛躺下準備美美的睡一覺的時候, 突然來一記奪命電話Call镊叁,一接起來發(fā)現(xiàn)是你老板3就恰!晦譬!“小王啊疤苹,剛剛上線的X.X.X版本出問題了啊,怎么樣操作會crash啊敛腌,導致新功能都無法使用了卧土,快定位一下是什么原因,抓緊hotpatch修復一下跋穹尤莺!”。心里一萬頭草泥馬呼嘯而過生棍,瞬間已經(jīng)滿頭大汗的你卻還要故作鎮(zhèn)靜地回答:“嗯颤霎,老板我馬上去看看,一定努力解決問題涂滴!” 急忙打開電腦的你捷绑,知道今夜注定無眠了。

是否又存在這樣的情形氢妈,你老板把大家都聚起來開了一個年初KPI目標制定會議,說到:“作為一個資深的技術團隊段多,app性能是我們技術團隊首抓的目標,其中很最要的一項就是app的崩潰率,去年我們app統(tǒng)計出來的崩潰率是千分之五蟆技,而我們的競爭對手的崩潰率只有萬分之五熊楼,相差了10倍!今年我們要趕超他們觉啊,最起碼也要和他們持平拣宏。” 你甚是贊同杠人,但是你心里卻又有點懷疑勋乾,對方的開發(fā)資源是我們的好幾倍而且個個都是資深老司機宋下,我們團隊里卻大多都是應屆生小鮮肉,這KPI能完成么辑莫?

大白健康系統(tǒng)--APP運行時Crash自動修復系統(tǒng)正是為了應對這些狀況学歧,解決問題而誕生的。

APP運行時Crash自動修復+捕獲系統(tǒng)的設計初衷各吨,就是為了降低app的crash率枝笨。利用Objective-C語言的動態(tài)特性,采用AOP(Aspect Oriented Programming) 面向切面編程的設計思想揭蜒,做到無痕植入横浑。能夠自動在app運行時實時捕獲導致app崩潰的破環(huán)因子,然后通過特定的技術手段去化解這些破壞因子屉更,使app免于崩潰徙融,照樣可以繼續(xù)正常運行,為app的持續(xù)運轉保駕護航偶垮。

<section class="" style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px;">

Chapter 2 - 功能簡介

</section>

APP運行時Crash自動修復系統(tǒng) 的主要功能张咳,可以用一句話來簡單的概括:對業(yè)務代碼的零侵入性地將原本會導致app崩潰的crash抓取住,消滅掉似舵,保證app繼續(xù)正常地運行脚猾,再將crash的具體信息提取出來,實時返回給用戶砚哗。

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

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

image

結果肯定會導致app的崩潰龙助,因為testObj是一個UIButton對象,而UIButton并沒有實現(xiàn) someMethod: 這個方法蛛芥,所以向testObj發(fā)送someMethod:這個方法的時候提鸟,將會導致該方法無法在相關的方法列表里找到,最終導致app的crash仅淑。

但是通過我們的crash防護系統(tǒng)称勋,調(diào)用這段代碼時app并不會崩潰,同時XCode的Console如下:

image

可見對應的crash的信息(crash類型涯竟,原因赡鲜,調(diào)用棧信息)均可以完整的打印在XCode的Console中。

說明我們的大白系統(tǒng)已經(jīng)捕捉到了這個crash庐船,將該crash消滅掉并且吐出來該crash的完整信息银酬。

當然目前系統(tǒng)的功能并沒有強大到可以把所有的crash都處理掉,不過一些常見的高頻次發(fā)生的crash筐钟,系統(tǒng)均會針對他們一一處理揩瞪。目前可以處理掉的crash類型具體有以下幾種:

  • unrecognized selector crash

  • KVO crash

  • NSNotification crash

  • NSTimer crash

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

  • NSString crash (字符串操作的crash)

  • Bad Access crash (野指針)

  • UI not on Main Thread Crash (非主線程刷UI(機制待改善))

對于每種類型的crash篓冲,安全系統(tǒng)都采取不同的方式李破,進行了對應的處理宠哄。

<section class="" style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px;">

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

</section>

前面已經(jīng)提過,目前的安全防護系統(tǒng)可以覆蓋到8中類型的Crash喷屋,接下來將一一詳細介紹這8種類型的Crash的防護的實現(xiàn)的具體原理:

<section class="" style="margin: 30px 0px 0px 8px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px; text-align: left; color: rgb(60, 112, 198);">

3.1 Unrecognized Selector類型crash防護</section>

3.1.1 crash產(chǎn)生原因

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

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

image

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

image

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

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

讓我們看一下方法調(diào)用在運行時的過程。

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

  1. 首先恶耽,在相應操作的對象中的緩存方法列表中找調(diào)用的方法密任,如果找到,轉向相應實現(xiàn)并執(zhí)行偷俭。

  2. 如果沒找到浪讳,在相應操作的對象中的方法列表中找調(diào)用的方法,如果找到涌萤,轉向相應實現(xiàn)執(zhí)行

  3. 如果沒找到淹遵,去父類指針所指向的對象中執(zhí)行1,2.

  4. 以此類推负溪,如果一直到根類還沒找到透揣,轉向攔截調(diào)用,走消息轉發(fā)機制川抡。

  5. 如果沒有重寫攔截調(diào)用的方法辐真,程序報錯。

3.1.3 攔截調(diào)用

在方法調(diào)用中說到了崖堤,如果沒有找到方法就會轉向攔截調(diào)用侍咱。

那么什么是攔截調(diào)用呢?

攔截調(diào)用就是密幔,在找不到調(diào)用的方法程序崩潰之前楔脯,你有機會通過重寫NSObject的四個方法來處理:

image

攔截調(diào)用的整個流程即Objective-C的消息轉發(fā)機制。其具體流程如下圖:

image

由上圖可見胯甩,在一個函數(shù)找不到時昧廷,runtime提供了三種方式去補救:

  1. 調(diào)用resolveInstanceMethod給個機會讓類添加這個實現(xiàn)這個函數(shù)

  2. 調(diào)用forwardingTargetForSelector讓別的對象去執(zhí)行這個函數(shù)

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

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

3.1.4 unrecognized selector crash 防護方案

既然可以補救溉苛,我們完全也可以利用消息轉發(fā)機制來做文章镜廉。那么問題來了,在這三個步驟里面愚战,選擇哪一步去改造比較合適呢娇唯。

這里我們選擇了第二步forwardingTargetForSelector來做文章齐遵。原因如下:

  1. resolveInstanceMethod需要在類的本身上動態(tài)添加它本身不存在的方法,這些方法對于該類本身來說冗余的

  2. forwardInvocation可以通過NSInvocation的形式將消息轉發(fā)給多個對象塔插,但是其開銷較大梗摇,需要創(chuàng)建新的NSInvocation對象,并且forwardInvocation的函數(shù)經(jīng)常被使用者調(diào)用想许,來做多層消息轉發(fā)選擇機制伶授,不適合多次重寫

  3. forwardingTargetForSelector可以將消息轉發(fā)給一個對象,開銷較小流纹,并且被重寫的概率較低糜烹,適合重寫

選擇了forwardingTargetForSelector之后,可以將NSObject的該方法重寫漱凝,做以下幾步的處理:

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

  2. 動態(tài)為樁類添加對應的Selector疮蹦,用一個通用的返回0的函數(shù)來實現(xiàn)該SEL的IMP

  3. 將消息直接轉發(fā)到這個樁類對象上。

流程圖如下:

image

注意如果對象的類本事如果重寫了forwardInvocation方法的話茸炒,就不應該對forwardingTargetForSelector進行重寫了愕乎,否則會影響到該類型的對象原本的消息轉發(fā)流程。

通過重寫NSObject的forwardingTargetForSelector方法壁公,我們就可以將無法識別的方法進行攔截并且將消息轉發(fā)到安全的樁類對象中感论,從而可以使app繼續(xù)正常運行。

<section class="" style="margin: 30px 0px 0px 8px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px; text-align: left; color: rgb(60, 112, 198);">

3.2 KVO類型crash防護</section>

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

KVO,即:Key-Value Observing贮尖,它提供一種機制笛粘,當指定的對象的屬性被修改后,則對象就會接受收到通知湿硝。簡單的說就是每次指定的被觀察的對象的屬性被修改后薪前,KVO就會自動通知相應的觀察者了。

KVO機制在iOS的很多開發(fā)場景中都會被使用到关斜。不過如果一不小心使用不當?shù)脑捠纠ǎ瑫е麓罅康腸rash問題。所以如果能找到一種方法能夠自動抓取這些由于開發(fā)者粗心所導致的KVO Crash問題的話痢畜,是有一定的價值的垛膝。

首先我們來看看通過會導致KVO Crash的兩種情形:

KVO的被觀察者dealloc時仍然注冊著KVO導致的crash,見下圖

image

添加KVO重復添加觀察者或重復移除觀察者(KVO注冊觀察者與移除觀察者不匹配)導致的crash丁稀,見下圖

image

3.2.2 KVO crash 防護方案

通常一個對象的KVO關系圖如下:

image

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

如果觀察者和keypath的數(shù)量一多,很容易理不清楚被觀察對象整個KVO關系线衫,導致被觀察者在dealloc的時候凿可,還殘存著一些關系沒有被注銷。 同時還會導致KVO注冊觀察者與移除觀察者不匹配的情況發(fā)生。

筆者曾經(jīng)還遇到過在多線程的情況下枯跑,導致KVO重復添加觀察者或移除觀察者的情況惨驶。這類問題通常多數(shù)發(fā)生的比較隱蔽,不容易從代碼的層面去排查敛助。

由上可見多數(shù)由于KVO而導致的crash原因是由于被觀察對象的KVO關系圖混亂導致粗卜。那么如何來管理混亂的KVO關系呢∧苫鳎可以讓被觀察對象持有一個KVO的delegate续扔,所有和KVO相關的操作均通過delegate來進行管理,delegate通過建立一張map來維護KVO整個關系评疗。如下圖:

image

這樣做的好處有兩個:

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

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

被swizzle的方法分別是:

image

關于

image

方法改造流程如下圖:

image

通過上面的流程加匈,將observerd對象的所有kvo相關的observer信息全部轉移到KVOdelegate上存璃,并且避免了相同kvoinfo被重復添加多次的可能性。

關于

image

方法改造流程如下圖:

image

移除一個keypath的Observer時雕拼,當delegate的kvoInfoMap中找不到key為該keypath的時候纵东,說明此時delegate并沒有持有對應keypath的observer,即說明移除了一個不匹配的觀察者啥寇,此時如果再繼續(xù)操作會導致app崩潰偎球,所以應該及時中斷流程,然后統(tǒng)計異常信息辑甜。

當keypath對應的KVOInfo列表(infoArray)為空的時候衰絮,說明此時delegate已經(jīng)不再持有任何和keypath相關的observer了。這時應該調(diào)用原有removeObserver的方法將delegate對應的觀察者移除磷醋。

注意到在檢查遍歷infoArray的時侯猫牡,除了要刪除對應的info信息,還多了一步檢查info.observer == nil的過程邓线,是因為如果observer為nil淌友,那么此時如果keypath對應的值變化的話,也會因為找不到observer而崩潰骇陈,所以需要做這一步來阻止該種情況的發(fā)生震庭。

關于

image

方法改造流程如下圖:

image

delegate對于observeValueForKeyPath方法的修改最主要的地方是,在于將對應的響應方法轉移給真正的KVO Observer你雌,通過keyInfoMap找到keypath對應的KVOInfo里面預先存儲好的observer器联,然后調(diào)用observer原本的響應方法。

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

最后逛球,針對 KVO的被觀察者dealloc時仍然注冊著KVO導致的crash 的情況千元,可以將NSObject的dealloc swizzle, 在object dealloc的時候自動將其對應的kvodelegate所有和kvo相關的數(shù)據(jù)清空颤绕,然后將kvodelegate也置空幸海。避免出現(xiàn)KVO的被觀察者dealloc時仍然注冊著KVO而產(chǎn)生的crash.

<section class="" style="margin: 30px 0px 0px 8px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px; text-align: left; color: rgb(60, 112, 198);">

3.3 NSNotification類型crash防護</section>

3.3.1 crash 產(chǎn)生原因

當一個對象添加了notification之后,如果dealloc的時候奥务,仍然持有notification,就會出現(xiàn)NSNotification類型的crash氯葬。

NSNotification類型的crash多產(chǎn)生于程序員寫代碼時候犯疏忽挡篓,在NSNotificationCenter添加一個對象為observer之后,忘記了在對象dealloc的時候移除它。

所幸的是,蘋果在iOS9之后專門針對于這種情況做了處理酷宵,所以在iOS9之后,即使開發(fā)者沒有移除observer煌抒,Notification crash也不會再產(chǎn)生了这溅。

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

3.3.2 NSNotification crash 防護方案

NSNotification Crash的防護原理很簡單膜廊, 利用method swizzling hook NSObject的dealloc函數(shù)爪瓜,再對象真正dealloc之前先調(diào)用一下

[[NSNotificationCenter defaultCenter] removeObserver:self]

即可丹喻。

注意到并不是所有的對象都需要做以上的操作,如果一個對象從來沒有被NSNotificationCenter 添加為observer的話藏研,在其dealloc之前調(diào)用removeObserver完全是多此一舉。 所以我們hook了NSNotificationCenter的

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

函數(shù)涧卵,在其添加observer的時候,對observer動態(tài)添加標記flag却嗡。這樣在observer dealloc的時候,就可以通過flag標記來判斷其是否有必要調(diào)用removeObserver函數(shù)了帝牡。

<section class="" style="margin: 30px 0px 0px 8px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px; text-align: left; color: rgb(60, 112, 198);">

3.4 NSTimer類型crash防護</section>

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

在程序開發(fā)過程中,大家會經(jīng)常使用定時任務崭别,但使用NSTimer的 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:接口做重復性的定時任務時存在一個問題:NSTimer會強引用target實例糯笙,所以需要在合適的時機invalidate定時器,否則就會由于定時器timer強引用target的關系導致target不能被釋放,造成內(nèi)存泄露,甚至在定時任務觸發(fā)時導致crash结耀。 crash的展現(xiàn)形式和具體的target執(zhí)行的selector有關愿卒。

與此同時,如果NSTimer是無限重復的執(zhí)行一個任務的話,也有可能導致target的selector一直被重復調(diào)用且處于無效狀態(tài),對app的CPU剔蹋,內(nèi)存等性能方面均是沒有必要的浪費律想。

所以,很有必要設計出一種方案,可以有效的防護NSTimer的濫用問題姨裸。

3.4.2 NSTimer crash 防護方案

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

那么解決NSTimer的問題的關鍵點在于以下兩點:

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

  2. 是否找到一個合適的時機令蛉,在確定NSTimer已經(jīng)失效的情況下,讓NSTimer自動invalidate

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

image.gif

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

同時NSTimer強引用stubTarget,而stubTarget弱引用target函卒,這樣target和NSTimer之間的關系也就是弱引用了辆憔,意味著target可以自由的釋放撇眯,從而解決了循環(huán)引用的問題报嵌。

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

Step 1. swizzle NSTimer中scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 相關的方法熊榛,在新方法中動態(tài)創(chuàng)建stubTarget對象锚国,stubTarget對象弱引用持有原有的target,selector玄坦,timer血筑,targetClass等properties。然后將原target分發(fā)stubTarget上煎楣,selector回調(diào)函數(shù)為stubTarget的fireProxyTimer豺总,流程如下圖:

image.gif

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

image.gif

因為stubTarget的介入择懂,原有的target已經(jīng)可以不受NSTimer強引用的牽制喻喳,而自由的釋放。

由上圖流程可知困曙,當NSTimer的回調(diào)函數(shù)fireProxyTimer:被執(zhí)行的時候表伦,會自動判斷原target是否已經(jīng)被釋放谦去,如果釋放了,意味著NSTimer已經(jīng)無效蹦哼,此時如果還繼續(xù)調(diào)用原有target的selector很有可能會導致crash鳄哭,而且是沒有必要的。所以此時需要將NSTimer invalidate纲熏,然后統(tǒng)計上報錯誤數(shù)據(jù)妆丘。如此一來就做到了NSTimer在合適的時機自動invalidate。

<section class="" style="margin: 30px 0px 0px 8px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px; text-align: left; color: rgb(60, 112, 198);">

3.5 Container類型crash防護</section>

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

Container 類型的crash 指的是容器類的crash赤套,常見的有NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的crash飘痛。 一些常見的越界、插入nil等錯誤操作均會導致此類crash發(fā)生容握。 由于產(chǎn)生的原因比較簡單宣脉,就不展開來描述了。

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

3.5.2 Container crash 防護方案

Container crash 類型的防護方案也比較簡單谈跛,針對于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些常用的會導致崩潰的API進行method swizzling羊苟,然后在swizzle的新方法中加入一些條件限制和判斷,從而讓這些API變的安全感憾,這里就不展開來具體描述了蜡励。

<section class="" style="margin: 30px 0px 0px 8px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px; text-align: left; color: rgb(60, 112, 198);">

3.6 NSString類型crash防護</section>

NSString/NSMutableString 類型的crash的產(chǎn)生原因和防護方案與Container crash很相像,這里也不展開來描述了阻桅。

<section class="" style="margin: 30px 0px 0px 8px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px; text-align: left; color: rgb(60, 112, 198);">

3.7 野指針類型crash防護</section>

3.7.1 野指針crash 產(chǎn)生原因

在App的所有Crash中凉倚,訪問野指針導致的Crash占了很大一部分,野指針類型crash的表現(xiàn)為:Exception Type:SIGSEGV嫂沉,Exception Codes: SEGV_ACCERR 或者 如下圖:

image.gif

解決野指針導致的crash往往是一件棘手的事情稽寒,一來產(chǎn)生crash 的場景不好復現(xiàn),二來crash之后console的信息提供的幫助有限趟章。 XCode本身為了便于開放調(diào)試時發(fā)現(xiàn)野指針問題杏糙,提供了Zombie機制,能夠在發(fā)生野指針時提示出現(xiàn)野指針的類蚓土,從而解決了開發(fā)階段出現(xiàn)野指針的問題宏侍。然而針對于線上產(chǎn)生的野指針問題,依舊沒有一個比較好的辦法來定位問題蜀漆。

所以谅河,因為野指針出現(xiàn)概率高而且難定位問題,非常有必要針對于野指針專門做一層防護措施。

3.7.2 野指針crash 防護方案

野指針問題的解決思路方向其實很容易確定旧蛾,XCode提供了Zombie的機制來排查野指針的問題莽龟,那么我們這邊可以實現(xiàn)一個類似于Zombie的機制,加上對zombie實例的全部方法攔截機制 和 消息轉發(fā)機制锨天,那么就可以做到在野指針訪問時不Crash而只是crash時相關的信息毯盈。

同時還需要注意一點:因為zombie的機制需要在對象釋放時保留其指針和相關內(nèi)存占用,隨著app的進行病袄,越來越多的對象被創(chuàng)建和釋放搂赋,這會導致內(nèi)存占用越來越大,這樣顯然對于一個正常運行的app的性能有影響益缠。所以需要一個合適的zombie對象釋放機制脑奠,確定zombie機制對內(nèi)存的影響是有限度的。

improve版的zombie機制的實現(xiàn)主要分為以下四個環(huán)節(jié):

step 1. method swizzling替換NSObject的allocWithZone方法幅慌,在新的方法中判斷該類型對象是否需要加入野指針防護宋欺,如果需要,則通過objc_setAssociatedObject為該對象設置flag標記胰伍,被標記的對象后續(xù)會進入zombie流程

流程圖如下:

image.gif

做flag標記是因為很多系統(tǒng)類齿诞,比如NSString,UIView等創(chuàng)建骂租,釋放非常頻繁祷杈,而這些實例發(fā)生野指針概率非常低∩基本都是我們自己寫的類才會有野指針的相關問題但汞,所以通過在創(chuàng)建時 設置一個標記用來過濾不必要做野指針防護的實例,提高方案的效率互站。

同時做判斷是否要加入標記的條件里面私蕾,我們加入了黑名單機制,是因為一些特定的類是不適用于添加到zombie機制的云茸,會發(fā)生崩潰(例如:NSBundle)是目,而且所以和zombie機制相關的類也不能加入標記谤饭,否則會在釋放過程中循環(huán)引用和調(diào)用标捺,導致內(nèi)存泄漏甚至棧溢出。

step 2. method swizzling替換NSObject的dealloc方法揉抵,對flag標記的對象實例調(diào)用objc_destructInstance亡容,釋放該實例引用的相關屬性,然后將實例的isa修改為HTZombieObject冤今。通過objc_setAssociatedObject 保存將原始類名保存在該實例中闺兢。

流程圖如下:

image.gif

調(diào)用objc_destructInstance的原因:

這里參考了系統(tǒng)在Object-C Runtime 中NSZombies實現(xiàn),dealloc最后會調(diào)到objectdispose函數(shù),在這個函數(shù)里面 其實也做了三件事情屋谭,

  1. 調(diào)用objc_destructInstance釋放該實例引用的相關實例

  2. 將該實例的isa修改為stubClass脚囊,接受任意方法調(diào)用

  3. 釋放該內(nèi)存

官方文檔對objc_destructInstance的解釋為:

Destroys an instance of a class without freeing memory and removes any associated references this instance might have had.

說明objc_destructInstance會釋放與實例相關聯(lián)的引用,但是并不釋放該實例等內(nèi)存桐磁。

Step 3. 在HTZombieObject 通過消息轉發(fā)機制forwardingTargetForSelector處理所有攔截的方法悔耘,根據(jù)selector動態(tài)添加能夠處理方法的響應者HTStubObject 實例,然后通過 objc_getAssociatedObject 獲取之前保存該實例對應的原始類名我擂,統(tǒng)計錯誤數(shù)據(jù)衬以。

流程圖如下:

image.gif

HTZombieObject的處理和unrecognized selector crash的處理是一樣,主要的目的就是攔截所有傳給HTZombieObject的函數(shù)校摩,用一個返回為空的函數(shù)來替換看峻,從而達到程序不崩潰的目的。

Step 4. 當退到后臺或者達到未釋放實例的上限時衙吩,則在ht_freeSomeMemory方法中調(diào)用原有dealloc方法釋放所有被zombie化的實例

綜上所述互妓,可以用下圖總結一下bad access類型crash的防護流程:

image.gif

3.7.3 相關的風險

  1. 做了野指針防護,通過動態(tài)插入一個空實現(xiàn)的方法來防止出現(xiàn)Crash坤塞,但是業(yè)務層面的表現(xiàn)難以確定车猬,可能會進入業(yè)務異常的狀態(tài)。需要擬定一下如何展現(xiàn)該問題給用戶的方案尺锚。

  2. 由于做了延時釋放若干實例珠闰,對系統(tǒng)總內(nèi)存會產(chǎn)生一定影響,目前將內(nèi)存的緩沖區(qū)開到2M左右瘫辩,所以應該沒有很大的影響伏嗜,但還是可能潛在一些風險。

  3. 延時釋放實例是根據(jù)相關功能代碼會聚焦在某一個時間段調(diào)用的假設前提下伐厌,所以野指針的zombie保護機制只能在其實例對象仍然緩存在zombie的緩存機制時才有效承绸,若在實例真正釋放之后,再調(diào)用野指針還是會出現(xiàn)Crash挣轨。

<section class="" style="margin: 30px 0px 0px 8px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px; text-align: left; color: rgb(60, 112, 198);">

3.8 非主線程刷UI類型crash防護(UI not on Main Thread)</section>

在非主線程刷UI將會導致app運行crash军熏,有必要對其進行處理。

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

- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;

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

來將對應的刷UI的操作轉移到主線程上,同時統(tǒng)計錯誤信息晤锹。

但是真正實施了之后摩幔,發(fā)現(xiàn)這三個方法并不能完全覆蓋UIView相關的所有刷UI到操作,但是如果要將全部到UIView的刷UI的方法統(tǒng)計起來并且swizzle鞭铆,感覺略笨拙而且不高效或衡。

所以作者依舊在尋找,看是否有更好的方案來解決該問題。

<section class="" style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-size: 16px;">

Chapter 4 - 使用手冊

</section>

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

1. 配置需要防護的crash類型

可以根據(jù)自身需要封断,選擇一定的crash防護配置斯辰,通過以下的接口進行配置:

- (void)configSafetyGuardService:(HTSafetyGuardType)SafetyGuardType;

其中可以配置的SafetyGuardType有:

  • HTSafetyGuardType_None

  • HTSafetyGuardType_All

  • HTSafetyGuardType_UnrecognizedSelector

  • HTSafetyGuardType_KVO

  • HTSafetyGuardType_BadAccess

  • HTSafetyGuardType_Notification

  • HTSafetyGuardType_Timer

  • HTSafetyGuardType_Container

  • HTSafetyGuardType_String

  • HTSafetyGuardType_UI

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

2. 實時 開啟/暫停 安全防護功能

配置完畢之后坡疼,需要調(diào)用- (void)start;來開啟防護椒涯,防護的開關是實時的(無需重啟app),可以在任意的時刻選擇 開啟/關閉 防護功能回梧。

通過- (BOOL)isWorking接口可以獲取當前防護功能的狀態(tài)废岂。

通過- (void)start接口實時開啟防護功能

通過- (void)stop接口實時關閉防護功能

3. 配置白名單和黑名單,指定對應的想 加上/去掉 安全防護功能的類和對象

由于不同類實現(xiàn)的特殊性狱意,考慮到可能某些類并不需要開啟防護功能湖苞。 所以提供了黑名單的功能。 在黑名單里面的類本身以及其子類详囤,都不會進入防護的范圍财骨。

白名單的出現(xiàn)是因為作者在開發(fā)的時候發(fā)現(xiàn)一些系統(tǒng)自帶的類是沒有必要進入防護范圍的,所以將整體防護的范圍調(diào)整到所有用戶自定義的類里面藏姐。 但是之后又發(fā)現(xiàn)絕大多數(shù)的crash和一些常用的系統(tǒng)的類(例如NSString隆箩,NSDictionary,UIView等等)有很強的聯(lián)系羔杨,針對于這些常用的系統(tǒng)類還是很有必要開啟防護的捌臊。所以針對這些需要防護的系統(tǒng)類,專門提供了白名單的功能兜材。

注意:野指針類型的防護理澎,由于其特殊性,不適用于這套白名單和黑名單曙寡。 其自身會維護一套新的黑白名單糠爬,詳見:3.7 野指針類型Crash防護

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

出現(xiàn)了crash皂林,并且被我們的系統(tǒng)捕捉到加以處理之后琳轿,用戶可能還需要進一步的處理簇爆,例如上傳埋點等硕糊。這時可以通過設置一個handler來實現(xiàn), HTExceptionHandler會將crash的信息通過HTCrashInfo的形式來返回添祸。

HTCrashInfo內(nèi)包含了:

  • 導致crash的類型:crashType

  • crash線程的調(diào)用棧:callStackSymbols

  • crash的具體描述信息:crashDescription

  • 擴展信息:userinfo

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

由于目前sdk還未經(jīng)過完整的功能測試和性能測試寻仗,故暫不開放對應的sdk刃泌。等作者覺得項目質量達到了一定的標準之后,會將項目sdk開放出來。如果對該項目感興趣耙替,可以聯(lián)系 taozeyu890217@126.com 亚侠,歡迎一起研究。

</section>

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末俗扇,一起剝皮案震驚了整個濱河市硝烂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铜幽,老刑警劉巖滞谢,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異除抛,居然都是意外死亡狮杨,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門到忽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來橄教,“玉大人,你說我怎么就攤上這事喘漏』さ” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵翩迈,是天一觀的道長持灰。 經(jīng)常有香客問我,道長负饲,這世上最難降的妖魔是什么搅方? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮绽族,結果婚禮上姨涡,老公的妹妹穿的比我還像新娘。我一直安慰自己吧慢,他們只是感情好涛漂,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著检诗,像睡著了一般匈仗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逢慌,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天悠轩,我揣著相機與錄音,去河邊找鬼攻泼。 笑死火架,一個胖子當著我的面吹牛鉴象,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播何鸡,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼纺弊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了骡男?” 一聲冷哼從身側響起淆游,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎隔盛,沒想到半個月后犹菱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡吮炕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年已亥,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片来屠。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡虑椎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出俱笛,到底是詐尸還是另有隱情捆姜,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布迎膜,位于F島的核電站泥技,受9級特大地震影響,放射性物質發(fā)生泄漏磕仅。R本人自食惡果不足惜珊豹,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望榕订。 院中可真熱鬧店茶,春花似錦、人聲如沸劫恒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽两嘴。三九已至丛楚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間憔辫,已是汗流浹背趣些。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留贰您,地道東北人坏平。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓拢操,卻偏偏與公主長得像,于是被迫代替她去往敵國和親功茴。 傳聞我的和親對象是個殘疾皇子庐冯,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

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

  • 一年有兩個情人節(jié)--情人節(jié)和七夕,每年被虐一次還不夠返劲,還要加倍的虐玲昧。 就在前幾天,對七夕還沒有什么特別的感覺篮绿,只是...
    米斯特左腦閱讀 905評論 6 3
  • 連續(xù)5天孵延,每天都有看笑來的專欄,每次都會看別人寫的留言亲配,每次都會留言尘应。為什么以前都是斷斷續(xù)續(xù)地看,沒有堅持吼虎,現(xiàn)在卻...
    婷下來思考閱讀 340評論 0 0
  • 最近經(jīng)歷了一次情緒的波動犬钢,現(xiàn)在回想起來很簡單, 就是聽聞大老板認可了一位男同事思灰,卻好像沒有看到我的優(yōu)點玷犹。拿我和男同...
    Candier閱讀 230評論 0 0