iOS-Crash 防护

APP 的崩溃问题,一直以来都是开发过程中重中之重的问题。日常开发阶段的崩溃,发现后还能够立即处理。但是一旦发布上架的版本出现问题,就需要紧急加班修复 BUG,再更新上架新版本了。在这个过程中, 说不定会因为崩溃而导致关键业务中断、用户存留率下降、品牌口碑变差、生命周期价值下降等,最终导致流失用户,影响到公司的发展。

当然,避免崩溃问题的最好办法就是不产生崩溃。在开发的过程中就要尽可能地保证程序的健壮性。但是,人又不是机器,不可能不犯错。不可能存在没有 BUG 的程序。但是如果能够利用一些语言机制和系统方法,设计一套防护系统,使之能够有效的降低 APP 的崩溃率,那么不仅 APP 的稳定性得到了保障,而且最重要的是可以减少不必要的加班。

防护原理简介和常见 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(找不到类方法实现) 造成的崩溃问题。

1.Method Swizzling 方法的封装

由于这几种常见 Crash 的防护都需要用到 Method Swizzling 技术。所以我们可以为 NSObject 新建一个分类,将 Method Swizzling 相关的方法封装起来。

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

#import 

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 

@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

2.Unrecognized Selector 防护

2.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消息转发步骤图

这里我们选择第二步(消息接受者重定向)来进行拦截。因为 -forwardingTargetForSelector 方法可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写。

具体步骤如下:

  1. 给 NSObject 添加一个分类,在分类中实现一个自定义的 -ysc_forwardingTargetForSelector: 方法;
  2. 利用 Method Swizzling 将 -forwardingTargetForSelector: 和 -ysc_forwardingTargetForSelector: 进行方法交换。
  3. 在自定义的方法中,先判断当前对象是否已经实现了消息接受者重定向和消息重定向。如果都没有实现,就动态创建一个目标类,给目标类动态添加一个方法。
  4. 把消息转发给动态生成类的实例对象,由目标类动态创建的方法实现,这样 APP 就不会崩溃了。
    实现代码如下:
#import "NSObject+SelectorDefender.h"
#import "NSObject+MethodSwizzling.h"
#import 

@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
2.2 unrecognized selector sent to class(找不到类方法实现)

同对象方法一样,如果被调用的类方法没有实现,那么同样也会导致 APP 崩溃。

例如,有这样一个类,声明了一个 + (id)aClassFunc; 的类方法, 但是并没有实现,就像下边的 YSCObject 这样。

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

@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 

@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

将 2.1 和 2.2 结合起来就可以拦截所有未实现的类方法和对象方法了。

3. KVC Crash

KVC Crash的常见原因

KVC(Key Value Coding),即键值编码,提供一种机制来间接访问对象的属性。而不是通过调用 Setter 、 Getter 方法进行访问。

KVC 日常使用造成崩溃的原因通常有以下几个:

1. key 不是对象的属性,造成崩溃。
2. keyPath 不正确,造成崩溃。
3. key 为 nil,造成崩溃。
4. value 为 nil,为非对象设值,造成崩溃。

常见的使用 KVC 造成崩溃代码:

/********************* KVCCrashObject.h 文件 *********************/
#import 

@interface KVCCrashObject : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) NSInteger age;

@end

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

@implementation KVCCrashObject

@end


/********************* ViewController.m 文件 *********************/
#import "ViewController.h"
#import "KVCCrashObject.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
 
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {    
//    1. key 不是对象的属性,造成崩溃
//    [self testKVCCrash1];

//    2. keyPath 不正确,造成崩溃
//    [self testKVCCrash2];

//    3. key 为 nil,造成崩溃
//    [self testKVCCrash4];

//    4. value 为 nil,为非对象设值,造成崩溃
//    [self testKVCCrash4];
}

/**
 1. key 不是对象的属性,造成崩溃
 */
- (void)testKVCCrash1 {
    // 崩溃日志:[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key XXX.;
    
    KVCCrashObject *objc = [[KVCCrashObject alloc] init];
    [objc setValue:@"value" forKey:@"address"];
}

/**
 2. keyPath 不正确,造成崩溃
 */
- (void)testKVCCrash2 {
    // 崩溃日志:[ valueForUndefinedKey:]: this class is not key value coding-compliant for the key XXX.
    
    KVCCrashObject *objc = [[KVCCrashObject alloc] init];
    [objc setValue:@"后厂村路" forKeyPath:@"address.street"];
}

/**
 3. key 为 nil,造成崩溃
 */
- (void)testKVCCrash3 {
    // 崩溃日志:'-[KVCCrashObject setValue:forKey:]: attempt to set a value for a nil key

    NSString *keyName;
    // key 为 nil 会崩溃,如果传 nil 会提示警告,传空变量则不会提示警告
    
    KVCCrashObject *objc = [[KVCCrashObject alloc] init];
    [objc setValue:@"value" forKey:keyName];
}

/**
 4. value 为 nil,造成崩溃
 */
- (void)testKVCCrash4 {
    // 崩溃日志:[ setNilValueForKey]: could not set nil as the value for the key XXX.
    
    // value 为 nil 会崩溃
    KVCCrashObject *objc = [[KVCCrashObject alloc] init];
    [objc setValue:nil forKey:@"age"];
}

@end
KVC的setter和getter方法
Setter方法

系统在执行 **setValue:forKey: **方法时,会把 key 和 value 作为输入参数,并尝试在接收调用对象的内部,给属性 key 设置 value 值。
通过以下几个步骤:

1. 按顺序查找名为  **set: **、 **_set: **、    **setIs: **方法。如果找到方法,则执行该方法,使用输入参数设置变量,则   **setValue:forKey:** 完成执行。如果没找到方法,则执行下一步。
2. 访问类的**accessInstanceVariablesDirectly** 属性。如果    accessInstanceVariablesDirectly 属性返回    YES ,就按顺序查找名为   **_ **、**_is **、**** 、**is** 的实例变量,如果找到了对应的实例变量,则使用输入参数设置变量。则   setValue:forKey: 完成执行。如果未找到对应的实例变量,或者   **accessInstanceVariablesDirectly** 属性返回    NO 则执行下一步。
3. 调用**setValue: forUndefinedKey: **方法,并引发崩溃。
  • 相关代码:
KVCCrashObject *objc = [[KVCCrashObject alloc] init];
[objc setValue:@"value" forKey:@"name"];
Getter方法

系统在执行**valueForKey: **方法时,会将给定的 key 作为输入参数,在调用对象的内部进行以下几个步骤:

  1. 按顺序查找名为get is_ 的访问方法。如果找到,调用该方法,并继续执行步骤 5。否则继续向下执行步骤 2。
  2. 搜索形如countOf objectInAtIndex: AtIndexes: 的方法。
  3. 如果实现了countOf 方法,并且实现了objectInAtIndex: 和 AtIndexes: 这两个方法的任意一个方法,系统就会以 NSArray 为父类,动态生成一个类型为 NSKeyValueArray 的集合类对象,并调用上边的实现方法,将结果直接返回。
    • 如果对象还实现了形如get:range: 的方法,系统也会在必要的时候自动调用。
    • 如果上述操作不成功则继续向下执行步骤 3。
    • 如果上边两步失败,系统就会查找形如countOfenumeratorOf memberOf: 的方法。系统会自动生成一个 NSSet 类型的集合类对象,该对象响应所有 NSSet 方法并将结果返回。如果查找失败,则执行步骤 4。
  4. 如果上边三步失败,系统就会访问类的accessInstanceVariablesDirectly 方法。
    • 如果返回 YES ,就按顺序查找名为 _ 、 _is 、 is 的实例变量。如果找到了对应的实例变量,则直接获取实例变量的值。并继续执行步骤 5。
    • 如果返回 NO ,或者未找到对应的实例变量,则继续执行步骤 6。
  5. 分为三种情况:
    • 如果检索到的属性值是对象指针,则直接返回结果。
    • 如果检索到的属性值是 NSNumber 支持的基础数据类型,则将其存储在 NSNumber 实例中并返回该值。
    • 如果检索到的属性值是 NSNumber 不支持的数据类型,则转换为 NSValue 对象并返回该对象。
  6. 如果一切都失败了,调用 valueForUndefinedKey: ,并引发崩溃。
KVC Crash 防护方案

从 Setter 方法 和 Getter 方法 可以看出:

  • setValue:forKey: 执行失败会调用 setValue: forUndefinedKey: 方法,并引发崩溃。
  • valueForKey: 执行失败会调用 valueForUndefinedKey: 方法,并引发崩溃。
    所以,为了进行 KVC Crash 防护,我们就需要重写 setValue: forUndefinedKey: 方法和 valueForUndefinedKey: 方法。重写这两个方法之后,就可以防护 1. key 不是对象的属性 和 2. keyPath 不正确 这两种崩溃情况了。

那么 3. key 为 nil,造成崩溃 的情况,该怎么防护呢?

我们可以利用 Method Swizzling 方法,在 NSObject 的分类中将 setValue:forKey: 和 ysc_setValue:forKey: 进行方法交换。然后在自定义的方法中,添加对 key 为 nil 这种类型的判断。

注意:这里我看到另外一个开发者不是很建议 Hook 掉系统的 setValue:forKey: 方法,说是为了尽可能少的对系统方法产生逻辑判断。这里我也持保留意见。

iOS 中的 crash 防护(二)KVC 造成的 crash

还有最后一种 4. value 为 nil,为非对象设值,造成崩溃 的情况。

在 NSKeyValueCoding.h 文件中,有一个 setNilValueForKey: 方法。上边的官方注释给了我们答案。

在调用 setValue:forKey: 方法时,系统如果查找到名为 set: 方法的时候,会去检测 value 的参数类型,如果参数类型为 NSNmber 的标量类型或者是 NSValue 的结构类型,但是 value 为 nil 时,会自动调用 setNilValueForKey: 方法。这个方法的默认实现会引发崩溃。

所以为了防止这种情况导致的崩溃,我们可以通过重写 setNilValueForKey: 来解决。

至此,上文提到的 KVC 使用不当造成的四种类型崩溃就都解决了。下面我们来看下具体实现代码。

  • 具体防护代码:
/********************* NSObject+KVCDefender.h 文件 *********************/
#import 

@interface NSObject (KVCDefender)

@end

/********************* NSObject+KVCDefender.m 文件 *********************/
#import "NSObject+KVCDefender.h"
#import "NSObject+MethodSwizzling.h"

@implementation NSObject (KVCDefender)

// 不建议拦截 `setValue:forKey:` 方法
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        // 拦截 `setValue:forKey:` 方法,替换自定义实现
        [NSObject yscDefenderSwizzlingInstanceMethod:@selector(setValue:forKey:)
                                       withMethod:@selector(ysc_setValue:forKey:)
                                        withClass:[NSObject class]];

    });
}

- (void)ysc_setValue:(id)value forKey:(NSString *)key {
    if (key == nil) {
        NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> setNilValueForKey]: could not set nil as the value for the key %@.",NSStringFromClass([self class]),self,key];
        NSLog(@"%@", crashMessages);
        return;
    }

    [self ysc_setValue:value forKey:key];
}

- (void)setNilValueForKey:(NSString *)key {
    NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> setNilValueForKey]: could not set nil as the value for the key %@.",NSStringFromClass([self class]),self,key];
    NSLog(@"%@", crashMessages);
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key: %@,value:%@'",NSStringFromClass([self class]),self,key,value];
    NSLog(@"%@", crashMessages);
}

- (nullable id)valueForUndefinedKey:(NSString *)key {
    NSString *crashMessages = [NSString stringWithFormat:@"crashMessages :[<%@ %p> valueForUndefinedKey:]: this class is not key value coding-compliant for the key: %@",NSStringFromClass([self class]),self,key];
    NSLog(@"%@", crashMessages);
    
    return self;
}

@end

你可能感兴趣的:(iOS-Crash 防护)