iOS 防止應用崩潰

面試網(wǎng)易有道,面試官在問 Runtime 時,提到了關于應用崩潰的問題,應該如何避免。

好吧,果然是道高一尺魔高一丈。這個問題主要涉及 Runtime 相關的知識,面試前我還特意寫了一篇博文 iOS Runtime 學習,用來總結學習這方面的知識,應對面試,結果還是把我問到了,為什么每次覺得已經(jīng)夠深入了,卻還是不夠深入呢?為什么每次面試都能問到我不會的呢?感覺看了那么多面試題,還是有不會的。

沒辦法,記錄一下吧,雖然面試涼涼,但這確實是一個很好的問題。

應用崩潰的原因有很多,先解決 方法找不到 unrecognized selector sent to instance 而產(chǎn)生的崩潰吧。

一、實現(xiàn)原理

說到實現(xiàn)原理,首先需要了解一個問題,這里又涉及到一道面試題:一個對象收到一個無法響應的方法到程序崩潰,這之間發(fā)生了什么?

答:當一個方法找不到的時候,會走攔截調用和消息轉發(fā)流程。

大致流程是:

1、對象調用方法時,系統(tǒng)會先調用該類的 +resolveInstanceMethod: 方法進行判斷,如果返回 YES, 則表示能接受消息,NO 表示不能接受消息并進入第下步。

2、系統(tǒng)會調用該類的 -forwardingTargetForSelector:,在這個方法中,可以把這個消息轉發(fā)給其他對象。敲黑板,這里是重點。

3、如果第二部返回 nil,那么首先它會發(fā)送 -methodSignatureForSelector: 消息,獲得函數(shù)的參數(shù)和返回值類型。如果 -methodSignatureForSelector: 返回nil,Runtime則會發(fā)出 -doesNotRecognizeSelector: 消息,程序這時也就掛掉了。如果返回了一個函數(shù)簽名,Runtime 就會創(chuàng)建一個 NSInvocation 對象并發(fā)送 -forwardInvocation:消息給目標對象。

這里只簡要說明一下,在 iOS Runtime 學習 中有詳細說明。如果對 Runtime 不熟悉,建議先了解 Runtime,再來看本文,就能很容易理解了。

二、實現(xiàn)

1、給 NSObject 添加一個分類,再在這個分類中自定義一個方法,-my_forwardingTargetForSelector:,在 +load 方法中,交換 NSObject 的 forwardingTargetForSelector:my_forwardingTargetForSelector:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod([NSObject class], @selector(forwardingTargetForSelector:));
        Method swizzledMethod = class_getInstanceMethod([NSObject class], @selector(my_forwardingTargetForSelector:));
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

2、在自定義的方法中,先判斷當前對象是否已經(jīng)實現(xiàn)了消息轉發(fā)方法,如果沒有實現(xiàn),就動態(tài)創(chuàng)建一個類,給這個類動態(tài)添加一個方法,再把消息轉發(fā)給這個動態(tài)創(chuàng)建的方法,這樣就不會崩潰了。

- (id)my_forwardingTargetForSelector:(SEL)aSelector {
    
    // 獲取NSObject的消息轉發(fā)方法
    SEL sel = NSSelectorFromString(@"forwardingTargetForSelector:");
    Method method = class_getInstanceMethod(NSClassFromString(@"NSObject"), sel);
    // 獲取當前類的消息轉發(fā)方法
    Method _m = class_getInstanceMethod([self class],sel);
    
    // 類本身有沒有實現(xiàn)消息轉發(fā)流程
    BOOL transmit = method_getImplementation(_m) == method_getImplementation(method);
    
    // 有木有實現(xiàn)下一步消息轉發(fā)流程
    if (transmit) {
        // 判斷有沒有實現(xiàn)第三步消息轉發(fā)
        SEL sel1 = NSSelectorFromString(@"methodSignatureForSelector:");
        Method method1 = class_getInstanceMethod(NSClassFromString(@"NSObject"), sel1);
        
        Method _m1 = class_getInstanceMethod([self class], sel1);
        transmit = method_getImplementation(_m1) == method_getImplementation(method1);
        
        if (transmit) {
            // 創(chuàng)建一個新類
            NSString *errClassName = NSStringFromClass([self class]);
            NSString *errSel   = NSStringFromSelector(aSelector);
            NSLog(@"出問題的類,出問題的方法 == %@ %@", errClassName, errSel);
            
            NSString *className = @"CrachClass";
            Class cls = NSClassFromString(className);
            /// 如果類不存在 動態(tài)創(chuàng)建一個類
            if (!cls) {
                Class superCls = [NSObject class];
                cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
                /// 給類添加方法
                class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
                objc_registerClassPair(cls);
            }
            /// 如果類沒有對應的方法,則動態(tài)添加一個
            if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
                class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
            }
            /// 把消息轉發(fā)到當前動態(tài)生成類的實例上
            return [[NSClassFromString(className) alloc] init];
        }
    }
    return [self my_forwardingTargetForSelector:aSelector];
}
static int Crash(id slf, SEL selector) {
    return 0;
}

主要是注意兩點:

1:類有沒有實現(xiàn) forwardingTargetForSelector: 函數(shù);
2:類有沒有實現(xiàn) methodSignatureForSelector: 函數(shù),實現(xiàn)了就表明類實現(xiàn)了消息轉發(fā)流程,那么不處理這個類。

三、其他類避免崩潰

還有一個常見的崩潰就是數(shù)組越界 index 2 beyond bounds [0 .. 1],對于這一類方法,方法是把系統(tǒng)的方法和自定義的方法交換,在自定義的方法里判斷是否越界。

根據(jù)上面的思路,給 NSArray 添加分類,交換 NSArray 的 objectAtIndex: 方法。

然鵝,但是,然并暖,程序還是崩潰了,信息如下:

Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndexedSubscript:]: index 2 beyond bounds [0 .. 1]'

恍然大悟,遂把需要交換的系統(tǒng)方法換成 objectAtIndexedSubscript:,而不是 objectAtIndex:,再次嘗試。

What(⊙o⊙)?

程序還是崩潰了,崩潰信息還跟上面一樣,這 TM 是幾個意思?再次仔細查看崩潰日志,終于發(fā)現(xiàn)問題了:-[__NSArrayI objectAtIndexedSubscript:]。這里是調用了 __NSArrayI 這個類的 -objectAtIndexedSubscript: 方法,而不是 NSArray 的方法。再來一次,具體代碼如下:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class __NSArrayI = NSClassFromString(@"__NSArrayI");
        Method originalMethod1 = class_getInstanceMethod(__NSArrayI, @selector(objectAtIndexedSubscript:));
        Method swizzledMethod1 = class_getInstanceMethod(__NSArrayI, @selector(my_objectAtIndexedSubscript:));
        method_exchangeImplementations(originalMethod1, swizzledMethod1);
    });
}

- (id)my_objectAtIndexedSubscript:(NSUInteger)idx {
    if (idx < 0 || idx >= self.count) {
        NSLog(@"數(shù)組越界了~~~~~");
        return self[0];
    }
    return [self my_objectAtIndexedSubscript:idx];
}

最后,終于成功了。

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

友情鏈接更多精彩內容