Crash 防護(hù)方案(一):Unrecognized Selector

原文 : 與佳期的個人博客(gonghonglou.com)

線上 APP Crash 是比較嚴(yán)重的問題,既影響用戶體驗(yàn)又不利于程序猿們的 KPI,我們應(yīng)當(dāng)盡量避免線上 Crash 的出現(xiàn),所以希望在 APP 發(fā)生 Crash 的時候能夠?qū)崿F(xiàn)自動防護(hù),雖然我們的手段可能會導(dǎo)致業(yè)務(wù)邏輯的出錯,但我們可以通過記錄 Crash,上報堆棧來及時解決問題,也比用戶 APP 崩潰掉要好得多。

文章參考網(wǎng)易iOS App運(yùn)行時Crash自動防護(hù)實(shí)踐 但給出了具體的實(shí)踐,有一些不同的方案,分析常用開源庫的做法或給出 Demo 來實(shí)現(xiàn)解決方案。

本系列文章防護(hù)方案應(yīng)對的的 Crash 有以下幾種:

  • Unrecognized Selector Crash
  • EXC_BAD_ACCESS crash
  • Container Crash
  • NSTimer Crash
  • KVO Crash
  • NSNotificationCenter Crash

Unrecognized Selector Crash

UIView *view = [UIView new];
[view performSelector:@selector(log)];

2019-07-08 14:07:35.895216+0800 GHLCrashGuard_Example[42376:3276094] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIView log]: unrecognized selector sent to instance 0x7f8e78c13950'

這算是 OC 里很經(jīng)典常見的 Crash 了,出現(xiàn)的原因是向一個對象發(fā)送了該對象無法響應(yīng)的消息,或者可以理解為調(diào)用一個對象不存在的方法發(fā)生的 Crash。簡單陳述一下 OC 消息發(fā)送轉(zhuǎn)發(fā)流程,從 objc_msgSend 方法開始,OC 消息發(fā)送機(jī)制看起來像是 objc_msgSend 返回了數(shù)據(jù),其實(shí) objc_msgSend 從不返回數(shù)據(jù)而是你的方法被調(diào)用后返回了數(shù)據(jù)。步驟:

1、檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發(fā),有了垃圾回收就不理會 retain, release 這些函數(shù)了。
2、檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執(zhí)行任何一個方法不會 Crash,因?yàn)闀缓雎缘簟?br> 3、如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應(yīng)的函數(shù)去執(zhí)行。
4、如果 cache 找不到就找一下方法分發(fā)表。
5、如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到 NSObject 類為止。

如果還找不到就要開始進(jìn)入動態(tài)方法解析了。當(dāng)向一個對象發(fā)送消息,發(fā)現(xiàn)對象無法響應(yīng),對象會依次執(zhí)行以下方法,這也是 ObjC 的運(yùn)行時給出的三次拯救程序崩潰的機(jī)會:

6、ObjC 運(yùn)行時會調(diào)用 +resolveInstanceMethod: 或者 +resolveClassMethod:,讓你有機(jī)會提供一個函數(shù)實(shí)現(xiàn)。如果你添加了函數(shù)并返回 YES,那運(yùn)行時系統(tǒng)就會重新啟動一次消息發(fā)送的過程,如果 resolve 方法返回 NO ,運(yùn)行時就會移到下一步,消息轉(zhuǎn)發(fā)(Message Forwarding)。
7、如果目標(biāo)對象實(shí)現(xiàn)了-forwardingTargetForSelector: 方法,Runtime 這時就會調(diào)用這個方法,給你把這個消息轉(zhuǎn)發(fā)給其他對象的機(jī)會。只要這個方法返回的不是 nil 和 self,整個消息發(fā)送的過程就會被重啟,當(dāng)然發(fā)送的對象會變成你返回的那個對象。否則,就會繼續(xù)
8、這一步是 Runtime 最后一次給你挽救的機(jī)會。首先它會發(fā)送 -methodSignatureForSelector: 消息獲得函數(shù)的參數(shù)和返回值類型。
如果 -methodSignatureForSelector: 返回 nil,Runtime 則會發(fā)出 -doesNotRecognizeSelector: 消息,程序這時也就掛掉了。
如果返回了一個函數(shù)簽名,Runtime 就會創(chuàng)建一個 NSInvocation 對象并發(fā)送 -forwardInvocation: 消息給目標(biāo)對象。

解決方案

Unrecognized Selector Crash 正是發(fā)生在 -doesNotRecognizeSelector: 消息里。防護(hù)方案是去 hook NSObject 的 -forwardingTargetForSelector: 方法,具體思路是:

1、如果對象(或者父類)沒有重寫 forwardInvocation: 方法,那么就認(rèn)為是調(diào)用出錯了
2、為了防止 Crash 這時新建一個干凈的 GHLCrashGuardProxy 對象,把方法轉(zhuǎn)發(fā)給 GHLCrashGuardProxy
3、GHLCrashGuardProxy 在 resolveInstanceMethod: 中動態(tài)的創(chuàng)建一個返回空的方法,然后執(zhí)行該方法防止 Crash

這里我們選擇 hook forwardingTargetForSelector 方法的原因是:

1、resolveInstanceMethod 需要在類的本身上動態(tài)添加它本身不存在的方法,這些方法對于該類本身來說冗余的
2、forwardInvocation 可以通過 NSInvocation 的形式將消息轉(zhuǎn)發(fā)給多個對象,但是其開銷較大,需要創(chuàng)建新的 NSInvocation 對象,并且 forwardInvocation 的函數(shù)經(jīng)常被使用者調(diào)用,來做多層消息轉(zhuǎn)發(fā)選擇機(jī)制,不適合多次重寫
3、forwardingTargetForSelector 可以將消息轉(zhuǎn)發(fā)給一個對象,開銷較小,并且被重寫的概率較低,適合重寫

cheaptalk.png

代碼實(shí)現(xiàn)

Hook 示例:

#import "NSObject+GHLCrashGuard.h"
#import <JRSwizzle/JRSwizzle.h>
#import "GHLUnrecognizedSelectorManager.h"

@implementation NSObject (GHLCrashGuard)

+ (void)load {
    // Unrecognized Selector
    [self jr_swizzleMethod:@selector(forwardingTargetForSelector:) withMethod:@selector(ghl_forwardingTargetForSelector:) error:nil];
}

- (id)ghl_forwardingTargetForSelector:(SEL)aSelector {
    
    return [[GHLUnrecognizedSelectorManager sharedInstance] handleObject:self forwardingTargetForSelector:aSelector];
}

@end

GHLUnrecognizedSelectorManager 查詢是否重寫 forwardInvocation: 方法并做轉(zhuǎn)發(fā)操作:

- (id)handleObject:(__unsafe_unretained id)object forwardingTargetForSelector:(SEL)aSelector {
    
    if (![self needGuard:[object class]]) {
        return nil;
    }
    
    NSLog(@"[%@ %@]: unrecognized selector sent to instance %@", [object class], NSStringFromSelector(aSelector), object);
    
    return [GHLCrashGuardProxy new];
}

- (BOOL)needGuard:(Class)cls {
    
    // 如果重寫了 forwardInvocation,說明自己要處理,這里直接返回
    if ([self methodHasOverwrited:@selector(forwardInvocation:) cls:cls]) {
        return NO;
    }
    return YES;
}

// 判斷 cls 是否重寫了 sel 方法,遞歸調(diào)用判斷但不包括 NSObject
- (BOOL)methodHasOverwrited:(SEL)sel cls:(Class)cls {
    
    unsigned int methodCount = 0;
    Method *methods = class_copyMethodList(cls, &methodCount);
    for (int i = 0; i < methodCount; i++) {
        Method method = methods[i];
        if (method_getName(method) == sel) {
            free(methods);
            return YES;
        }
    }
    free(methods);
    
    // 可能父類實(shí)現(xiàn)了這個 sel,一直遍歷到基類 NSObject 為止
    if ([cls superclass] != [NSObject class]) {
        return [self methodHasOverwrited:sel cls:[cls superclass]];
    }
    return NO;
}

GHLCrashGuardProxy 的 resolveInstanceMethod: 實(shí)現(xiàn):

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    class_addMethod([self class], sel, imp_implementationWithBlock(^{
        // 收集堆棧,上報 Crash
        NSLog(@"%@", [NSThread callStackSymbols]);        
        
        return nil;
    }), "@@:");
    return YES;
}

這里為了方便展示用了 NSThread 的 callStackSymbols 來收集堆棧,但這個方法只能收集當(dāng)前線程的對戰(zhàn),實(shí)際工作時可以選擇 backtrace_symbols 方法或者更好的堆棧收集工具。

The return value describes the call stack backtrace of the current thread at the moment this method was called.

Demo 地址:GHLCrashGuard:GHLCrashGuard/Classes/Unrecognized Selector

后記

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容