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

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

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

經過本文,您將瞭解到:ios

  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-Crashgit


1. Crash 防禦系統開篇

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

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

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

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


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

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

經過 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(找不到類方法實現) 形成的崩潰問題。ui


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: 消息,程序也就崩潰了。

Runtime 消息轉發步驟圖.png

這裏咱們選擇第二步(消息接受者重定向)來進行攔截。由於 -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


參考資料


相關文章
相關標籤/搜索