JJException保護(hù)iOS App不閃退

保護(hù)App,一般常見(jiàn)的問(wèn)題不會(huì)導(dǎo)致閃退惋鸥,增強(qiáng)App的健壯性贺嫂,同時(shí)會(huì)將錯(cuò)誤拋出來(lái),根據(jù)每個(gè)App自身的日志渠道記錄捂龄,下次迭代修復(fù)那些問(wèn)題.

  • Unrecognized Selector Sent to Instance

  • NSArray,NSMutableArray,NSDictonary,NSMutableDictionary

  • KVO

  • Zombie Pointer

  • NSTimer

  • NSNotification

Unrecognized Selector Sent to Instance

由于Objective-c是Message機(jī)制释涛,而且對(duì)象在轉(zhuǎn)換的時(shí)候,會(huì)有拿到的對(duì)象和預(yù)期不一致倦沧,所以會(huì)有方法找不到的情況唇撬,在找不到方法時(shí),查找方法將會(huì)進(jìn)入方法Forward流程,系統(tǒng)給了三次補(bǔ)救的機(jī)會(huì)展融,所以我們要解決這個(gè)問(wèn)題窖认,在這三次均可以解決這個(gè)問(wèn)題

forward
  • resolveInstanceMethod:(SEL)sel
    這是實(shí)例化方法沒(méi)有找到方法,最先執(zhí)行的函數(shù),首先會(huì)流轉(zhuǎn)到這里來(lái)扑浸,返回值是BOOL,沒(méi)有找到就是NO,找到就返回YES,如果要解決就需要再當(dāng)前的實(shí)例中加入不存在的Selector,并綁定IMP烧给,示例如下:
static void xxxInstanceName(id self, SEL cmd, id value) {
    NSLog(@"resolveInstanceMethod %@", value);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"resolveInstanceMethod");
    
    NSMethodSignature* sign = [self methodSignatureForSelector:selector];
    if (!sign) {
        class_addMethod([self class], sel, (IMP)xxxInstanceName, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
  • forwardingTargetForSelector:(SEL)aSelector

如果resolveInstanceMethod沒(méi)有處理,將進(jìn)行到forwardingTargetForSelector這步來(lái)喝噪,這時(shí)候你可以返回nil础嫡,你也可以用一個(gè)Stub對(duì)象來(lái)接住,把消息流程流轉(zhuǎn)到了你的Stub那邊了酝惧,然后在你的Stub里添加不存在的Selector榴鼎,這樣就不會(huì)crash了,示例如下:

- (id)forwardingTargetForSelectorSwizzled:(SEL)selector{
    NSMethodSignature* sign = [self methodSignatureForSelector:selector];
    if (!sign) {
        id stub = [[UnrecognizedSelectorHandle new] autorelease];
        class_addMethod([stub class], selector, (IMP)unrecognizedSelector, "v@:");
        return stub;
    }
    return [self forwardingTargetForSelectorSwizzled:selector];
}
  • methodSignatureForSelector:(SEL)aSelector

  • forwardInvocation:(NSInvocation *)anInvocation

這兩個(gè)方法一起說(shuō)晚唇,因?yàn)樗麄冎g有關(guān)聯(lián)巫财,

  1. 當(dāng)methodSignatureForSelector返回nil時(shí),會(huì)Crash
  2. 如果methodSignatureForSelector返回一個(gè)定義好的NSMethodSignature哩陕,但是沒(méi)有實(shí)現(xiàn)forwardInvocation平项,也會(huì)閃退,如果實(shí)現(xiàn)了forwardInvocation悍及,會(huì)先返回到resolveInstanceMethod然后再才會(huì)到forwardInvocation
  3. 當(dāng)流轉(zhuǎn)到forwardInvocation,通過(guò)以下方法:
[anInvocation invokeWithTarget:xxxtarget1];
[anInvocation invokeWithTarget:xxxtarget2];

還可以流轉(zhuǎn)到多個(gè)對(duì)象,[anInvocation invokeWithTarget:xxxtarget2]是為了讓不存在的方法有著陸點(diǎn)

  • doesNotRecognizeSelector:(SEL)aSelector
    執(zhí)行到這里的時(shí)候闽瓢,兩種情況:
  1. 當(dāng)methodSignatureForSelector返回一種任意的方法簽名的時(shí)候,也會(huì)進(jìn)入doesNotRecognizeSelector并鸵,但是不會(huì)閃退
  2. 當(dāng)methodSignatureForSelector返回nil時(shí)鸳粉,進(jìn)入doesNotRecognizeSelector就會(huì)閃退

根據(jù)以上流程,最終還是選擇流程2,原因如下:

  1. resolveInstanceMethod雖然可以解決問(wèn)題园担,給不存在的方法增加到示例中去届谈,會(huì)污染當(dāng)前示例
  2. forwardInvocation在三步中式最后一步,會(huì)導(dǎo)致流轉(zhuǎn)的周期變長(zhǎng)弯汰,而且會(huì)產(chǎn)生NSInvocation,性能不是最好的選擇

NSArray,NSMutableArray,NSDictonary,NSMutableDictionary

  • 類族(Class Cluster)

NSDictonary艰山,NSArray,NSString等,都使用了類族咏闪,這種模式最大的好處就是曙搬,可以隱藏抽象基類背后的復(fù)雜細(xì)節(jié),使用者只需調(diào)用基類簡(jiǎn)單的方法就可以返回不同的子類實(shí)例

  • Swizzle Hook

這里就不贅述Swizzle概念了鸽嫂,Google到處都是講解的纵装,這里給一個(gè)典型的例子:

swizzleInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:), @selector(hookObjectAtIndex:));

- (id) hookObjectAtIndex:(NSUInteger)index {
    if (index < self.count) {
        return [self hookObjectAtIndex:index];
    }
    handleCrashException(@"HookObjectAtIndex invalid index");
    return nil;
}

Zombie Pointer

讓野指針不閃退是模仿了XCode debug的Zombie Object,也參考了網(wǎng)易和美團(tuán)的做法,主要是以下步驟:

  1. Hook住dealloc方法
  2. 如果當(dāng)前示例在黑名單里据某,就把當(dāng)年前示例加入集合橡娄,并把當(dāng)前對(duì)象objc_destructInstance清理引用關(guān)系,并未真正釋放內(nèi)存癣籽,并將object_setClass設(shè)置成自己的中間對(duì)象
  3. Hook中間對(duì)象的方法挽唉,收到的消息都由中間對(duì)象來(lái)處理
  4. 維護(hù)的野指針集合滤祖,要么根據(jù)個(gè)數(shù)來(lái)維護(hù),要么根據(jù)總大小來(lái)維護(hù)瓶籽,當(dāng)滿了匠童,就需要真正釋放對(duì)象內(nèi)存free(obj)

存在的問(wèn)題:

  1. 需要單獨(dú)的內(nèi)存那些問(wèn)題對(duì)象
  2. 最后釋放內(nèi)存后,再訪問(wèn)時(shí)會(huì)閃退塑顺,這個(gè)方法只是一定程度延遲了閃退時(shí)間
  3. 需要后臺(tái)維護(hù)黑名單機(jī)制汤求,來(lái)指定那些問(wèn)題對(duì)象

KVO,NSTimer严拒,NSNotification

這三種放在一起首昔,是因?yàn)樗麄冎g有共同的特征,就是創(chuàng)建后糙俗,忘記銷毀會(huì)導(dǎo)致閃退,或者會(huì)有一些異常的情況预鬓,所以需要一種知道當(dāng)前創(chuàng)建者啥時(shí)候釋放巧骚,首先會(huì)想到dealloc,這樣會(huì)Hook的NSObject,在一定程度會(huì)影響性能,后面發(fā)現(xiàn)一種比較優(yōu)雅的方法,原理來(lái)自于Runtime源碼:

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARR ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
* Be warned that GC DOES NOT CALL THIS. If you edit this, also edit finalize.
* CoreFoundation and other clients do call this under GC.
**********************************************************************/
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = !UseGC && obj->hasAssociatedObjects();
        bool dealloc = !UseGC;

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        if (dealloc) obj->clearDeallocating();
    }

    return obj;
}

_object_remove_assocations會(huì)釋放所有的用AssociatedObject格二,所以我們Hook以下方法劈彪,只是列舉有代表性的,根據(jù)自身情況補(bǔ)齊添加的地方

  • KVO(addObserver:forKeyPath)

  • NSNotification(addObserver:selector)

objc_setAssociatedObject給當(dāng)前對(duì)象添加一個(gè)中間對(duì)象顶猜,當(dāng)前對(duì)象釋放時(shí)沧奴,會(huì)清理AssociatedObject數(shù)據(jù),AssociatedObject的中間對(duì)象將被清理釋放长窄,中間對(duì)象的dealloc方法將被執(zhí)行滔吠,最終清理被遺漏的監(jiān)聽(tīng)者。

  • NSTimer(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats)

NSTimer的問(wèn)題在于挠日,target默認(rèn)是強(qiáng)引用疮绷,如果用戶不手動(dòng)關(guān)閉NSTimer和置空,會(huì)存在內(nèi)存泄漏和異常情況嚣潜,所以用中間層來(lái)持有冬骚,用KVO和NSNotification的方法來(lái)清理

MRC

這里單獨(dú)說(shuō)下,為什么工程選擇了MRC懂算,因?yàn)樵贖ook集合類型的時(shí)候只冻,啟動(dòng)的時(shí)候就閃退了,Crash的地方在系統(tǒng)類里计技,Stack里顯示在CF這層喜德,這里只能猜測(cè)系統(tǒng)底層對(duì)ARC的支持不好導(dǎo)致的,后續(xù)改成MRC就沒(méi)有問(wèn)題酸役,所以這個(gè)需要繼續(xù)研究和追蹤住诸,如果有知道的同學(xué)記得告知我下

性能

本來(lái)是沒(méi)有打算注意性能這個(gè)問(wèn)題的驾胆,因?yàn)閺腍ook原理的角度來(lái)說(shuō),只是交換IMP的指向贱呐,時(shí)間復(fù)雜度來(lái)說(shuō)丧诺,只是在系統(tǒng)級(jí)別上增加了幾條邏輯判斷指令,所以這個(gè)影響是極小的奄薇,基本可以忽略驳阎,我經(jīng)過(guò)測(cè)試,循環(huán)1000000次馁蒂,沒(méi)有HOOK和HOOK相差0.0x秒的呵晚,所以減少Crash,來(lái)增加這么點(diǎn)時(shí)間復(fù)雜度來(lái)說(shuō)沫屡,是值得的饵隙。

不過(guò)最后說(shuō)一點(diǎn),就是dealloc確實(shí)需要注意沮脖,因?yàn)檫@里存在集合的操作金矛,所以要注意時(shí)間復(fù)雜度,dealloc執(zhí)行的很頻繁的勺届,而且主線程和子線程都會(huì)涉及到驶俊,尤其是主線程一定注意,否則會(huì)影響到UI的體驗(yàn)免姿。

https://github.com/jezzmemo/JJException

參考資料

https://github.com/opensource-apple/objc4/blob/master/runtime/objc-runtime-new.mm

大白健康系統(tǒng)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末饼酿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子胚膊,更是在濱河造成了極大的恐慌故俐,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件澜掩,死亡現(xiàn)場(chǎng)離奇詭異购披,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)肩榕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門刚陡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人株汉,你說(shuō)我怎么就攤上這事筐乳。” “怎么了乔妈?”我有些...
    開(kāi)封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵蝙云,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我路召,道長(zhǎng)勃刨,這世上最難降的妖魔是什么波材? 我笑而不...
    開(kāi)封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮身隐,結(jié)果婚禮上廷区,老公的妹妹穿的比我還像新娘。我一直安慰自己贾铝,他們只是感情好隙轻,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著垢揩,像睡著了一般玖绿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上叁巨,一...
    開(kāi)封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天斑匪,我揣著相機(jī)與錄音,去河邊找鬼锋勺。 笑死秤标,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的宙刘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼牢酵,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼悬包!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起馍乙,我...
    開(kāi)封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤布近,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后丝格,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體撑瞧,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年显蝌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了预伺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡曼尊,死狀恐怖酬诀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情骆撇,我是刑警寧澤瞒御,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站神郊,受9級(jí)特大地震影響肴裙,放射性物質(zhì)發(fā)生泄漏趾唱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一蜻懦、第九天 我趴在偏房一處隱蔽的房頂上張望甜癞。 院中可真熱鬧,春花似錦阻肩、人聲如沸带欢。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)乔煞。三九已至,卻和暖如春柒室,著一層夾襖步出監(jiān)牢的瞬間渡贾,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工雄右, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留空骚,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓擂仍,卻偏偏與公主長(zhǎng)得像囤屹,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子逢渔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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