iOS KVC底层原理分析

准备工作

KVC协议定义

KVCNSKeyValueCoding的简写,键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该机制来提供对其属性的间接访问。当对象符合键值编码时,其属性可通过字符串参数通过简洁、统一的消息传递接口进行寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。

KVC在Objective-C中的定义

KVC的定义都是对NSObject的扩展来实现的,查看setValueForKey方法,发现其在Foundation里面,而Foundation框架是不开源的,只能在苹果官方文档查找。见下图:

Foundation框架

KVC提供的API方法

  • 我们可以通过官方提供的文档进行查看(文章开头有链接)
  • 苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。

常用方法

对于所有继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用KVC,下面是KVC最为重要的四个方法:

   - (nullable id)valueForKey:(NSString *)key;                          // 直接通过Key来取值
   - (void)setValue:(nullable id)value forKey:(NSString *)key;          // 通过Key来设值
   - (nullable id)valueForKeyPath:(NSString *)keyPath;                  // 通过KeyPath来取值
   - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  // 通过KeyPath来设值

特殊方法

NSKeyValueCoding类别中还有其他的方法,当我们遇到适合的需求时,就能够派上用场了。方法如下:

// 默认返回YES,表示如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;

// KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;

// 和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

// 如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

KVC常用案例

  • 结构体的处理
    KVC在进行结构体处理时,需要用到NSValue,设值时,将结构体封装成NSValue,进行键值设值;取值同样返回NSValue,然后按照结构体格式进行解析,见下面代码:
    // 结构体
    ThreeFloats floats = {1.,2.,3.};
    // 封装成NSValue
    NSValue *value     = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    // 设值
    [person setValue:value forKey:@"threeFloats"];

    // 取值
    NSValue *value1    = [person valueForKey:@"threeFloats"];
    // 结构体解析
    ThreeFloats th;
    [value1 getValue:&th];
    NSLog(@"%f-%f-%f",th.x,th.y,th.z);
  • 字典处理(模型转换)
    字典可以实现与模型进行装换,也可以通过键值数组从模型中获取字典数据。实现代码如下:
- (void)dictionaryTest{
    // 字典
    NSDictionary* dict = @{
                           @"name":@"Cooci",
                           @"nick":@"KC",
                           @"subject":@"iOS",
                           @"age":@18,
                           @"length":@180
                           };
    // 模型
    LGStudent *p = [[LGStudent alloc] init];
    // 字典转模型
    [p setValuesForKeysWithDictionary:dict];

    // 键值数组
    NSArray *array = @[@"name",@"age"];
    // 从模型中获取响应的字典数据
    NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
    NSLog(@"%@",dic);
}

KVC设值取值顺序

KVC的使用相信是没什么难度的,但是它寻找key的过程是怎么样子的呢?以下就进行分析。

设值

当调用setValue:forKey:代码时,会有什么的内部操作呢?我在其官方文章中找到下图:

官方解释

上图的意思是:
setValue:forKey:的默认实现,给定keyvalue参数作为输入,尝试将名为key的属性设置为value,在接收调用的对象内部,使用以下过程:按顺序查找名为 set<Key>:_set<Key> 的第一个访问器。 如果找到,则使用输入值(或根据需要展开的值)调用它并完成。如果未找到简单访问器,并且类方法 accessInstanceVariablesDirectly返回 YES,则按顺序查找名称类似于 _<key>_is<Key><key>is<Key> 的实例变量。 如果找到,直接使用输入值(或解包值)设置变量并完成。
在未找到访问器或实例变量时,调用 setValue:forUndefinedKey:。 默认情况下,这会引发异常,但 NSObject的子类可能会提供特定于键的行为。

根据上面的解析可以总结为以下的几点:

  • 按顺序查找名为set<Key>_set<Key>或者setIs<Key>setter访问器顺序查找,如果找到就调用。只要实现任意一个,那么就会将调用这个方法,将属性的值设为传进来的值
  • 如果没有找到这些setter方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法。
  • 如果返回YESKVC机制会优先搜索该类里面有没有名为_<Key>的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以_<Key>命名的变量,KVC都可以对该成员变量赋值。
  • KVC机制再会继续搜索_is<Key><key>is<key>的成员变量,再给它们赋值。
  • 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。
[person setValue:@"newName" forKey:@"name"];为例,得出结论:
  • 优先通过setter方法,进行属性设置,调用顺序是:
    • setName
    • _setName
    • setIsName
  • 果以上方法均未找到,并且accessInstanceVariablesDirectly返回YES,则通过成员变量进行设置,顺序是:
    • _name
    • _isName
    • name
    • isName

注意:以上可以通过案例进行演示,我就不在这里演示了。

补充说明accessInstanceVariablesDirectly

尝试重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO,如果KVC没有找到set<Key>_set<Key>setIs<Key>相关方法时,会直接用setValue:forUndefinedKey:方法。我们用代码来测试一下上面的KVC机制:

@interface LGPerson : NSObject
{
    @public
        NSString *_isName;
        NSString *name;
        NSString *isName;
        NSString *_name;
}
@end
@implementation LGPerson

+(BOOL)accessInstanceVariablesDirectly{
    return NO;
}

-(id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"出现异常,该key不存在%@",key);
    return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
     NSLog(@"出现异常,该key不存在%@",key);
}

// 设置方法全部注释掉
// -(void)setName:(NSString*)name{
//     toSetName = name;
// }
// - (void)_setName:(NSString *)name{
//     NSLog(@"%s - %@",__func__,name);
// }
// - (void)setIsName:(NSString *)name{
//     NSLog(@"%s - %@",__func__,name);
// }


@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson* person = [LGPerson new];
        [person setValue:@"NewName" forKey:@"name"];
        NSString* name = [person valueForKey:@"name"];
        NSLog(@"value for key : %@",name);

        NSLog(@"取值_name:%@",person->_name);
        NSLog(@"取值_isName:%@",person->_isName);
        NSLog(@"取值name:%@",person->name);
        NSLog(@"取值isName:%@",person->isName);
    }
    return 0;
}

运行结果:


运行结果

这说明了重写+(BOOL)accessInstanceVariablesDirectly方法让其返回NO后,KVC找不到set<Key>等方法后,不再去找<Key>系列成员变量,而是直接调用setValue:forUndefinedKey:,如果我们自身的类不需要KVC机制的话可以这样子写。

KVC设值流程图
KVC设值流程图

取值

同理,在调用valueForKey:时候会发生什么呢?根据官方的文档得出:

valueForKey官方文档描述

根据官方文档得出valueForKey:的机制如下:

  • 首先按get<Key><Key>is<Key>_<Key>的顺序方法查找getter方法,找到的话会直接调用,如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。
  • 如果上面的getter没有找到,KVC则会查找countOf<Key>objectIn<Key>AtIndex<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>objectIn<Key>AtIndexAt<Key>Indexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名
  • 如果上面的方法没有找到,那么会同时查找countOf<Key>enumeratorOf<Key>memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>enumeratorOf<Key>memberOf<Key>组合的形式调用。
  • 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<Key>_is<Key><Key>is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:
  • 还没有找到的话,调用valueForUndefinedKey:
[person valueForKey:@"name"];为例
  • getter方法的调用顺序是:
    • getName
    • name
    • isName
    • _name
  • 如果以上方法没有找到,accessInstanceVariablesDirectly返回YES,则直接返回成员变量,获取顺序依然是:
    • _name
    • _isName
    • name
    • isName

注意:以上可以通过案例进行演示,我就不在这里演示了。

KVC取值流程图
KVC取值流程
代码验证KVC取值(需要的话就拷贝运行即可)
    @interface LGPerson : NSObject
    {
        @public
            NSString *_isName;
            NSString *name;
            NSString *isName;
            NSString *_name;
    }
    @end
    @implementation LGPerson

    +(BOOL)accessInstanceVariablesDirectly{
        return NO;
    }

    -(id)valueForUndefinedKey:(NSString *)key{
        NSLog(@"出现异常,该key不存在%@",key);
        return nil;
    }
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
         NSLog(@"出现异常,该key不存在%@",key);
    }

    // 设置方法全部注释掉
    // -(void)setName:(NSString*)name{
    //     toSetName = name;
    // }
    // - (void)_setName:(NSString *)name{
    //     NSLog(@"%s - %@",__func__,name);
    // }
    // - (void)setIsName:(NSString *)name{
    //     NSLog(@"%s - %@",__func__,name);
    // }

    // 取值方法
    //- (NSString *)getName{
    //    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)name{
    //    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)isName{
    //    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)_name{
    //    return NSStringFromSelector(_cmd);
    //}
    @end

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            LGPerson* person = [LGPerson new];
            [person setValue:@"NewName" forKey:@"name"];
            NSString* name = [person valueForKey:@"name"];
            NSLog(@"value for key : %@",name);

            NSLog(@"取值_name:%@",person->_name);
            NSLog(@"取值_isName:%@",person->_isName);
            NSLog(@"取值name:%@",person->name);
            NSLog(@"取值isName:%@",person->isName);
        }
        return 0;
    }

在KVC中使用keyPath

除了对当前对象的属性进行赋值外,还可以对其更深层的对象进行赋值。例如,对当前对象的location属性的country属性进行赋值。KVC进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。

    [person setValue:@"" forKeyPath:@"location.country"];

通过keyPath对数组进行取值时,并且数组中存储的对象类型都相同,可以通过valueForKeyPath:方法指定取出数组中所有对象的某个字段。例如下面例子中,通过valueForKeyPath:将数组中所有对象的name属性值取出,并放入一个数组中返回。

    NSArray *names = [array valueForKeyPath:@"name"];
例子展示以及运行结果

异常处理

当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个异常,并且应用程序Crash。见下图:

crash现象

重写以下两个方法,防止crash发生:

    -(id)valueForUndefinedKey:(NSString *)key{
        NSLog(@"出现异常,该key不存在%@",key);
        return nil;
    }
    
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
         NSLog(@"出现异常,该key不存在%@",key);
    }

再次运行程序,发现不再崩溃:

再次运行程序

为了合理处理KVC发出的异常,我们还可以这样子处理:

- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        [self setValue:@"" forKey:@”age”];
    } else {
        [super setNilValueForKey:key];
    }
}

自定义KVC的实现

根据苹果官方文档提供的设值、取值规则,我们可以自己进行KVC的自定义实现。见下面实现代码:

// KVC 自定义
@implementation NSObject (LGKVC)

// 设置
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
    // 1: 判断什么 key
    if (key == nil || key.length == 0) {
        return;
    }

    // 2: setter set<Key>: or _set<Key>,
    // key 要大写
    NSString *Key = key.capitalizedString;

    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];

    // 是否存在方法
    if ([self lg_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }

    // 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
    // 3:判断是否能够直接赋值实例变量——NO
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];

    }

    // 4: 间接变量
    // 获取 ivar -> 遍历 containsObjct -
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // 拼接成员变量
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];

    // 是否存在对应的变量
    if ([mArray containsObject:_key]) {
        // 4.2 获取相应的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 对相应的 ivar 设置值
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:_isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:key]) {
       Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }

    // 5:如果找不到相关实例

    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

// 取值
- (nullable id)lg_valueForKey:(NSString *)key{

    // 1:刷选key 判断非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // key 要大写
    NSString *Key = key.capitalizedString;

    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
        if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
            int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i<num-1; i++) {
                num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            }

            for (int j = 0; j<num; j++) {
                id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                [mArray addObject:objc];
            }
            return mArray;
        }
    }
#pragma clang diagnostic pop


    // 3:判断是否能够直接赋值实例变量-YES、NO
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }

    // 4.找相关实例变量进行赋值
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];

    // _<key> _is<Key> <key> is<Key>
    // _name -> _isName -> name -> isName
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    
    // 判断是否存在对应的成员变量
    if ([mArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }
    
    return @"";
}

#pragma mark **- 相关方法**

- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

- (id)performSelectorWithMethodName:(NSString *)methodName{

    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}

- (NSMutableArray *)getIvarListName{
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }

    free(ivars);
    return mArray;
}

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

推荐阅读更多精彩内容

  • 什么是KVC? KVC的全称叫Key-Value Coding,也叫做键值编码,在apple官方文档中是这么解释的...
    Joker_King阅读 660评论 0 3
  • YYModel的作用就是字典转模型,在了解YYModel前,我们先了解下KVC的知识。 KVC:也称之键值编码,是...
    我叫Vincent阅读 1,677评论 0 3
  • KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的...
    奋斗的郅博阅读 161评论 0 0
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,135评论 30 470
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,041评论 0 4