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

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 <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
2.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

将 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 <Foundation/Foundation.h>

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

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

  1. 按顺序查找名为get<Key> <key>is<Key>_<key> 的访问方法。如果找到,调用该方法,并继续执行步骤 5。否则继续向下执行步骤 2。
  2. 搜索形如countOf<Key> objectIn<Key>AtIndex: <key>AtIndexes: 的方法。
  3. 如果实现了countOf<Key> 方法,并且实现了objectIn<Key>AtIndex: 和 <key>AtIndexes: 这两个方法的任意一个方法,系统就会以 NSArray 为父类,动态生成一个类型为 NSKeyValueArray 的集合类对象,并调用上边的实现方法,将结果直接返回。
    • 如果对象还实现了形如get<Key>:range: 的方法,系统也会在必要的时候自动调用。
    • 如果上述操作不成功则继续向下执行步骤 3。
    • 如果上边两步失败,系统就会查找形如countOf<Key> 、 enumeratorOf<Key> memberOf<Key>: 的方法。系统会自动生成一个 NSSet 类型的集合类对象,该对象响应所有 NSSet 方法并将结果返回。如果查找失败,则执行步骤 4。
  4. 如果上边三步失败,系统就会访问类的accessInstanceVariablesDirectly 方法。
    • 如果返回 YES ,就按顺序查找名为 _<key> 、 _is<Key> 、 <key> 、 is<Key> 的实例变量。如果找到了对应的实例变量,则直接获取实例变量的值。并继续执行步骤 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<Key>: 方法的时候,会去检测 value 的参数类型,如果参数类型为 NSNmber 的标量类型或者是 NSValue 的结构类型,但是 value 为 nil 时,会自动调用 setNilValueForKey: 方法。这个方法的默认实现会引发崩溃。

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

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

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

@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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容

  • 本文首发于我的个人博客:「程序员充电站」[https://itcharge.cn]文章链接:「传送门」[https...
    ITCharge阅读 2,967评论 2 20
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32
  • 什么是KVC? KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实是指iOS的开...
    祀梦_阅读 854评论 0 7
  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    我的梦工厂阅读 880评论 1 8
  • 1.先了解踩盘目的。为以下提供真实、详细、有价值的参考:项目拿地价值,楼盘前期定位,制订价格策略,挖掘营销卖点,学...
    阿拉淄博音阅读 594评论 0 0