iOS 開發:『Crash 防護系統』(一)Unrecognized Selector

  • 本文首發於我的個人博客:『不羈閣』
  • 文章鏈接:傳送門
  • 本文更新時間:2019年08月23日12:15:21

本文是 『Crash 防護系統』系列 第一篇。
這個系列將會介紹如何設計一套 APP Crash 防護系統。這套系統採用 AOP(面向切面編程)的設計思想,利用 Objective-C語言的運行時機制,在不侵入原有項目代碼的基礎之上,通過在 APP 運行時階段對崩潰因素的的攔截和處理,使得 APP 能夠持續穩定正常的運行。

通過本文,您將瞭解到:

  1. Crash 防護系統開篇
  2. 防護原理簡介和常見 Crash
  3. Method Swizzling 方法的封裝
  4. Unrecognized Selector 防護
    4.1 unrecognized selector sent to instance(找不到對象方法的實現)
    4.2 unrecognized selector sent to class(找不到類方法實現)

文中示例代碼在: bujige / YSC-Avoid-Crash


1. Crash 防護系統開篇

APP 的崩潰問題,一直以來都是開發過程中重中之重的問題。日常開發階段的崩潰,發現後還能夠立即處理。但是一旦發佈上架的版本出現問題,就需要緊急加班修復 BUG,再更新上架新版本了。在這個過程中, 說不定會因爲崩潰而導致關鍵業務中斷、用戶存留率下降、品牌口碑變差、生命週期價值下降等,最終導致流失用戶,影響到公司的發展。

當然,避免崩潰問題的最好辦法就是不產生崩潰。在開發的過程中就要儘可能地保證程序的健壯性。但是,人又不是機器,不可能不犯錯。不可能存在沒有 BUG 的程序。但是如果能夠利用一些語言機制和系統方法,設計一套防護系統,使之能夠有效的降低 APP 的崩潰率,那麼不僅 APP 的穩定性得到了保障,而且最重要的是可以減少不必要的加班。

這套 Crash 防護系統被命名爲:『YSCDefender(防衛者)』。Defender 也是路虎旗下最硬派的越野車系。在電影《Tomb Raider》裏面,由 Angelina Jolie 飾演的英國女探險家 Lara Croft,所駕駛的就是一臺 Defender。Defender 也是我比較喜歡的車之一。

不過呢,這不重要。。。我就是爲這個項目起了個花裏胡哨的名字,並給這個名字賦予了一些無聊的意義。。。


2. 防護原理簡介和常見 Crash

Objective-C 語言是一門動態語言,我們可以利用 Objective-C 語言的 Runtime 運行時機制,對需要 Hook 的類添加 Category(分類),在各個分類的 +(void)load; 中通過 Method Swizzling 攔截容易造成崩潰的系統方法,將系統原有方法與添加的防護方法的 selector(方法選擇器) 與 IMP(函數實現指針)進行對調。然後在替換方法中添加防護操作,從而達到避免以及修復崩潰的目的。

通過 Runtime 機制可以避免的常見 Crash :

  1. unrecognized selector sent to instance(找不到對象方法的實現)
  2. unrecognized selector sent to class(找不到類方法實現)
  3. KVO Crash
  4. KVC Crash
  5. NSNotification Crash
  6. NSTimer Crash
  7. Container Crash(集合類操作造成的崩潰,例如數組越界,插入 nil 等)
  8. NSString Crash (字符串類操作造成的崩潰)
  9. Bad Access Crash (野指針)
  10. Threading Crash (非主線程刷 UI)
  11. NSNull Crash

這一篇我們先來講解下 unrecognized selector sent to instance(找不到對象方法的實現)unrecognized selector sent to class(找不到類方法實現) 造成的崩潰問題。


3. Method Swizzling 方法的封裝

由於這幾種常見 Crash 的防護都需要用到 Method Swizzling 技術。所以我們可以爲 NSObject 新建一個分類,將 Method Swizzling 相關的方法封裝起來。

/********************* NSObject+MethodSwizzling.h 文件 *********************/

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (MethodSwizzling)

/** 交換兩個類方法的實現
 * @param originalSelector  原始方法的 SEL
 * @param swizzledSelector  交換方法的 SEL
 * @param targetClass  類
 */
+ (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;

/** 交換兩個對象方法的實現
 * @param originalSelector  原始方法的 SEL
 * @param swizzledSelector 交換方法的 SEL
 * @param targetClass  類
 */
+ (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;

@end

/********************* NSObject+MethodSwizzling.m 文件 *********************/

#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>

@implementation NSObject (MethodSwizzling)

// 交換兩個類方法的實現
+ (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
    swizzlingClassMethod(targetClass, originalSelector, swizzledSelector);
}

// 交換兩個對象方法的實現
+ (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
    swizzlingInstanceMethod(targetClass, originalSelector, swizzledSelector);
}

// 交換兩個類方法的實現 C 函數
void swizzlingClassMethod(Class class, SEL originalSelector, SEL swizzledSelector) {

    Method originalMethod = class_getClassMethod(class, originalSelector);
    Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

    BOOL didAddMethod = class_addMethod(class,
                                        originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));

    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

// 交換兩個對象方法的實現 C 函數
void swizzlingInstanceMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    BOOL didAddMethod = class_addMethod(class,
                                        originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));

    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

@end

4. Unrecognized Selector 防護

4.1 unrecognized selector sent to instance(找不到對象方法的實現)

如果被調用的對象方法沒有實現,那麼程序在運行中調用該方法時,就會因爲找不到對應的方法實現,從而導致 APP 崩潰。比如下面這樣的代碼:

UIButton *testButton = [[UIButton alloc] init];
[testButton performSelector:@selector(someMethod:)];

testButton 是一個 UIButton 對象,而 UIButton 類中並沒有實現 someMethod: 方法。所以向 testButoon 對象發送 someMethod: 方法,就會導致 testButoon 對象無法找到對應的方法實現,最終導致 APP 的崩潰。

那麼有辦法解決這類因爲找不到方法的實現而導致程序崩潰的方法嗎?

我們從『 iOS 開發:『Runtime』詳解(一)基礎知識』知道了消息轉發機制中三大步驟:消息動態解析消息接受者重定向消息重定向。通過這三大步驟,可以讓我們在程序找不到調用方法崩潰之前,攔截方法調用。

大致流程如下:

  1. 消息動態解析:Objective-C 運行時會調用 +resolveInstanceMethod: 或者 +resolveClassMethod:,讓你有機會提供一個函數實現。我們可以通過重寫這兩個方法,添加其他函數實現,並返回 YES, 那運行時系統就會重新啓動一次消息發送的過程。若返回 NO 或者沒有添加其他函數實現,則進入下一步。
  2. 消息接受者重定向:如果當前對象實現了 forwardingTargetForSelector:,Runtime 就會調用這個方法,允許我們將消息的接受者轉發給其他對象。如果這一步方法返回 nil,則進入下一步。
  3. 消息重定向:Runtime 系統利用 methodSignatureForSelector: 方法獲取函數的參數和返回值類型。
    • 如果 methodSignatureForSelector: 返回了一個 NSMethodSignature 對象(函數簽名),Runtime 系統就會創建一個 NSInvocation 對象,並通過 forwardInvocation: 消息通知當前對象,給予此次消息發送最後一次尋找 IMP 的機會。
    • 如果 methodSignatureForSelector: 返回 nil。則 Runtime 系統會發出 doesNotRecognizeSelector: 消息,程序也就崩潰了。

這裏我們選擇第二步(消息接受者重定向)來進行攔截。因爲 -forwardingTargetForSelector 方法可以將消息轉發給一個對象,開銷較小,並且被重寫的概率較低,適合重寫。

具體步驟如下:

  1. 給 NSObject 添加一個分類,在分類中實現一個自定義的 -ysc_forwardingTargetForSelector: 方法;
  2. 利用 Method Swizzling 將 -forwardingTargetForSelector:-ysc_forwardingTargetForSelector: 進行方法交換。
  3. 在自定義的方法中,先判斷當前對象是否已經實現了消息接受者重定向和消息重定向。如果都沒有實現,就動態創建一個目標類,給目標類動態添加一個方法。
  4. 把消息轉發給動態生成類的實例對象,由目標類動態創建的方法實現,這樣 APP 就不會崩潰了。

實現代碼如下:

#import "NSObject+SelectorDefender.h"
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>

@implementation NSObject (SelectorDefender)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
 
        // 攔截 `-forwardingTargetForSelector:` 方法,替換自定義實現
        [NSObject yscDefenderSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:)
                                          withMethod:@selector(ysc_forwardingTargetForSelector:)
                                           withClass:[NSObject class]];
        
    });
}

// 自定義實現 `-ysc_forwardingTargetForSelector:` 方法
- (id)ysc_forwardingTargetForSelector:(SEL)aSelector {
    
    SEL forwarding_sel = @selector(forwardingTargetForSelector:);
    
    // 獲取 NSObject 的消息轉發方法
    Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
    // 獲取 當前類 的消息轉發方法
    Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);
    
    // 判斷當前類本身是否實現第二步:消息接受者重定向
    BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
    
    // 如果沒有實現第二步:消息接受者重定向
    if (!realize) {
        // 判斷有沒有實現第三步:消息重定向
        SEL methodSignature_sel = @selector(methodSignatureForSelector:);
        Method root_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);
        
        Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
        realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
        
        // 如果沒有實現第三步:消息重定向
        if (!realize) {
            // 創建一個新類
            NSString *errClassName = NSStringFromClass([self class]);
            NSString *errSel = NSStringFromSelector(aSelector);
            NSLog(@"出問題的類,出問題的對象方法 == %@ %@", errClassName, errSel);
            
            NSString *className = @"CrachClass";
            Class cls = NSClassFromString(className);
            
            // 如果類不存在 動態創建一個類
            if (!cls) {
                Class superClsss = [NSObject class];
                cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
                // 註冊類
                objc_registerClassPair(cls);
            }
            // 如果類沒有對應的方法,則動態添加一個
            if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
                class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
            }
            // 把消息轉發到當前動態生成類的實例對象上
            return [[cls alloc] init];
        }
    }
    return [self ysc_forwardingTargetForSelector:aSelector];
}

// 動態添加的方法實現
static int Crash(id slf, SEL selector) {
    return 0;
}

@end

4.2 unrecognized selector sent to class(找不到類方法實現)

同對象方法一樣,如果被調用的類方法沒有實現,那麼同樣也會導致 APP 崩潰。

例如,有這樣一個類,聲明瞭一個 + (id)aClassFunc; 的類方法, 但是並沒有實現,就像下邊的 YSCObject 這樣。

/********************* YSCObject.h 文件 *********************/
#import <Foundation/Foundation.h>

@interface YSCObject : NSObject

+ (id)aClassFunc;

@end

/********************* YSCObject.m 文件 *********************/
#import "YSCObject.h"

@implementation YSCObject

@end

如果我們直接調用 [YSCObject aClassFunc]; 就會導致崩潰。

找不到類方法實現的解決方法和之前類似,我們可以利用 Method Swizzling 將 +forwardingTargetForSelector:+ysc_forwardingTargetForSelector: 進行方法交換。

#import "NSObject+SelectorDefender.h"
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>

@implementation NSObject (SelectorDefender)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        // 攔截 `+forwardingTargetForSelector:` 方法,替換自定義實現
        [NSObject yscDefenderSwizzlingClassMethod:@selector(forwardingTargetForSelector:)
                                       withMethod:@selector(ysc_forwardingTargetForSelector:)
                                        withClass:[NSObject class]];
    });
}

// 自定義實現 `+ysc_forwardingTargetForSelector:` 方法
+ (id)ysc_forwardingTargetForSelector:(SEL)aSelector {
    SEL forwarding_sel = @selector(forwardingTargetForSelector:);
    
    // 獲取 NSObject 的消息轉發方法
    Method root_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel);
    // 獲取 當前類 的消息轉發方法
    Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel);
    
    // 判斷當前類本身是否實現第二步:消息接受者重定向
    BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
    
    // 如果沒有實現第二步:消息接受者重定向
    if (!realize) {
        // 判斷有沒有實現第三步:消息重定向
        SEL methodSignature_sel = @selector(methodSignatureForSelector:);
        Method root_methodSignature_method = class_getClassMethod([NSObject class], methodSignature_sel);
        
        Method current_methodSignature_method = class_getClassMethod([self class], methodSignature_sel);
        realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
        
        // 如果沒有實現第三步:消息重定向
        if (!realize) {
            // 創建一個新類
            NSString *errClassName = NSStringFromClass([self class]);
            NSString *errSel = NSStringFromSelector(aSelector);
            NSLog(@"出問題的類,出問題的類方法 == %@ %@", errClassName, errSel);
            
            NSString *className = @"CrachClass";
            Class cls = NSClassFromString(className);
            
            // 如果類不存在 動態創建一個類
            if (!cls) {
                Class superClsss = [NSObject class];
                cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
                // 註冊類
                objc_registerClassPair(cls);
            }
            // 如果類沒有對應的方法,則動態添加一個
            if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
                class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
            }
            // 把消息轉發到當前動態生成類的實例對象上
            return [[cls alloc] init];
        }
    }
    return [self ysc_forwardingTargetForSelector:aSelector];
}

// 動態添加的方法實現
static int Crash(id slf, SEL selector) {
    return 0;
}

@end

將 4.1 和 4.2 結合起來就可以攔截所有未實現的類方法和對象方法了。具體實現可參考代碼: bujige / YSC-Avoid-Crash


參考資料


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章