KVC 流程分析 自定义及异常处理

今天学习一下 KVC 深层次的东西 喜欢的可以进来看看,也许有你中意的哦~

1.成员变量&实例变量&属性
2.KVC初探
3.KVC赋值
4.KVC取值
5.KVC的异常处理
6.KVC的进阶用法
7.YYModel原理分析
8.category源码分析

1.成员变量&实例变量&属性 的区别是什么

//实例变量是一种特殊的成员变量
// class 类实例出来的变量 就是 实例变量  Btn 就是
@interface LGPerson : NSObject
{ //这个里面全部为成员变量
    @public
    NSString *Name; 
    UIButton *Btn ;  //实例变量
    id hello; //id 是一种特殊的class
}
// 这个是属性无疑  属性会有一个默认的  setter + getter 方法
@property (nonatomic, copy) NSString *nameX;
  1. 苹果早期的编译器是GCC 后面升级 变成 LLVM
  2. synthsize Name = _Name; //自动生成 setter 和 getter
  3. llvm之后 如果发现实例变量或者成员变量之后 没有匹配到实例变量的相应属性的时候 就会自动创建一个带 (_Name)

2.KVC初探

KVC 文档详情移步
先提几个问题

  1. KVC是什么 是一种 机制 -- 通过xx 间接访问成员变量
    ------------------------ -- 通过键值编码
  2. 有什么作用 进行一系列的键值编码

KVC使用

[self.textFiled setValue:[UIColor orangeColor] forKeyPath:@"_placeholderLabel.textColor"];

3.KVC赋值

当调用setValue:forKey: 代码时候,底层会

  • 优先调用 setKey: 属性值 方法,代码通过setter 方法完成设置。 注意这里的key是指成员变量名,首字母大小写要符合KVC的命名规范, 下同
  • 如果没有找到setName: 方法,KVC机制会检查 + (BOOL)accessInstanceVariablesDirectly 方法有没有返回YES,默认YES,如果你重写该方法为NO。KVC就直接执行setValue: forUndefineKey:方法,
    不过一般不会这么做,所以KVC机制会搜索该类里面有没有名为_key的成员变量,无论是.h | .m 里面定义,也不论用什么样的访问修饰符,只要存在_key命名的变量,KVC都可以对该成员变量赋值。
  • 如果该类没有setKey: 方法,也没有_key成员变量,KVC机制会搜索_isKey成员变量
  • 继续没有就 继续搜索 _key 和 _isKey 成员变量,KVC也会继续搜索key 和 isKey 的成员变量,在给他们赋值。
  • 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象 setValue:forUndefinedKey:方法,默认是抛出异常。

4.KVC取值

当调用valueForKey的代码时候,

  • 首先会按照 get <Key>,<key>,<Key>或_ <key> 顺序来查找 getter方法,找到这些方法就会直接调用,调用的过程遇到BOOL 或者Int 类型,会把他包装成NSNumber对象
  • 如果以上四个方法没有找打 KVC就会查找 countOf<Key> and objectIn<Key>AtIndex: 和 <key>AtIndexes 格式的方法。如果找到countOf<Key> 和另外两个方法中的一个,那么就会返回一个可以响应NSArray所有方法的代理集合(他是NSKeyValueArray, 是NSArray的子类),并返回该对象
  • 如果上面的方法没有找到,就会查找countOf<Key>, enumeratorOf<Key>, 和 memberOf<Key>, 如果这个三个方法都找到就会返回一个NSSet 方法代理集合,这个代理集合会发NSSet的消息,就会以countOfKey,enumeratorOfKey,memberOfKey组合的形式调用。

简单的说就是如果你在自己的类定义了KVC的实现,并且实现了上面的方法,那么你可以将返回的对象当做数组(NSArray)/集合(NSSet)用了。

  • 这个时候还没找到,就会检查类方法+(BOOL)accessInstanceVariablesDirectly,如果返回YES ,那么和之前的设值一样,会按_key,_isKey,key,isKey的顺序搜索成员变量名,这里不推荐这样做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了+(BOOL)accessInstanceVariablesDirectly返回NO的话,那么直接调用valueForUndefinedKey:
  • 最后再没找到就 调用 valueForUndefinedKey:

5.KVC的异常处理

几个比较常见的异常有

  1. 没有找到的一些值
[p setValue:nil forKey:@"subject"];

2.不存在的Key

[p setValue:@"hello" forKey:@"nickName"];

2.取值的时候不存在的Key

NSLog(@"%@",[p valueForKey:@"FKName"]);

需要对这些不安全的因素进行收集

// 对非对象类型赋值 不能设置空值
- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"%@的值不能为空",key);
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"不能对不存在的健赋值");
}

- (id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"不能对不存在的键取值");
    return @"error";
}

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError
{
    if (*ioValue == nil || inKey == nil || inKey.length == 0) {
        NSLog(@"value 可能为nil  或者key为nil或者空值");
        return NO;
    }
    return YES;
}

6.KVC的进阶用法

  • 先看用法
//控制器调用   
-(void)arrayFKDemo
{
    FKPerson *fp = FKPerson.new;
    fp.mouseArr = [NSMutableArray arrayWithObjects:@"mouse0", @"mouse1", @"mouse2", @"mouse3", nil];
    NSArray *arr = [fp valueForKey:@"mouse"]; // 动态成员变量
    NSLog(@"mouse = %@", arr);
}
[20759:5051180] mouse = (
    "mouse 0",
    "mouse 1",
    "mouse 2",
    "mouse 3")

// 个数 
- (NSUInteger)countOfMouse
{
    return [self.mouseArr count];
}

//// 获取值
- (id)objectInMouseAtIndex:(NSUInteger)index
{
    return [NSString stringWithFormat:@"mouse %lu", index];
}

// 是否包含这个成员对象
- (id)memberOfPens:(id)object {
    return [self.penArr containsObject:object] ? object : nil;
}

// 迭代器
- (id)enumeratorOfPens {
    // objectEnumerator
    return [self.penArr reverseObjectEnumerator];
}

在上面person里面是没有mouse 这个key的,但是只要实现了 countOf<Key> 和 objectIn<Key>AtIndex: 这连个方法,程序就不会蹦,照常进行

如果是set集合 就调用 memberOf<Key> 和 countOf<Key>, enumeratorOf<Key>其中一个方法 也是可以的 这里的用法可以根据官方文档来探索

7.YYModel原理分析

/// Returns the cached model class meta
+ (instancetype)metaWithClass:(Class)cls {
    if (!cls) return nil;
    static CFMutableDictionaryRef cache; //这里会声明一个字典,在下面做缓存使用,第一次进来缓存数据,
    static dispatch_once_t onceToken;
    static dispatch_semaphore_t lock;
    dispatch_once(&onceToken, ^{
        cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        lock = dispatch_semaphore_create(1);
    });
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
//第二次进来就会直接从全局的cache缓存里面去,性能高
    _YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls));
    dispatch_semaphore_signal(lock);
    if (!meta || meta->_classInfo.needUpdate) {
        meta = [[_YYModelMeta alloc] initWithClass:cls];
        if (meta) {
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
            CFDictionarySetValue(cache, (__bridge const void *)(cls), (__bridge const void *)(meta));
            dispatch_semaphore_signal(lock);
        }
    }
    return meta;
}
  • initWithClass 方法里面 先进去判断 黑白名单过滤操作 ,然后下一步会进行属性替换操作 (modelContainerPropertyGenericClass) 并进行处理(按照不同的类型处理)
+ (NSDictionary *)modelContainerPropertyGenericClass{
    return @{@"books" : FKSubModel.class,
             @"infoDict" : FKSubTModel.class,
             @"likedUserIds" : NSNumber.class
             };
}

方法进行到这一步之后会将里面各种不同的数据类型进行转换

+ (NSDictionary *)_yy_dictionaryWithJSON:(id)json {
    if (!json || json == (id)kCFNull) return nil;
    NSDictionary *dic = nil;
    NSData *jsonData = nil;
    if ([json isKindOfClass:[NSDictionary class]]) {
        dic = json;
    } else if ([json isKindOfClass:[NSString class]]) {
        jsonData = [(NSString *)json dataUsingEncoding : NSUTF8StringEncoding];
    } else if ([json isKindOfClass:[NSData class]]) {
        jsonData = json;
    }
    if (jsonData) {
        dic = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL];
        if (![dic isKindOfClass:[NSDictionary class]]) dic = nil;
    }
    return dic;
}
转换完了之后,就会得到一个jsonData并序列化,结果就会返回一个dic
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
    if (!dic || dic == (id)kCFNull) return NO;
    if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    

    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
    if (modelMeta->_keyMappedCount == 0) return NO;
    // 这里会拿到当前meta里面所有的map所有的类,发送一个响应的方法CFArrayApplyFunction
    if (modelMeta->_hasCustomWillTransformFromDictionary) {
        dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
        if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    }
    
    ModelSetContext context = {0};
    context.modelMeta = (__bridge void *)(modelMeta);
    context.model = (__bridge void *)(self);
    context.dictionary = (__bridge void *)(dic);
    
    //这个里面都会调用ModelSetWithPropertyMetaArrayFunction方法
    if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
        CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
        if (modelMeta->_keyPathPropertyMetas) {
            CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
        if (modelMeta->_multiKeysPropertyMetas) {
            CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
    } else {
        CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
                             CFRangeMake(0, modelMeta->_keyMappedCount),
                             ModelSetWithPropertyMetaArrayFunction,
                             &context);
    }
    
    if (modelMeta->_hasCustomTransformFromDictionary) {
        return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
    }
    return YES;
}

/**
 Apply function for model property meta, to set dictionary to model.
 
 @param _propertyMeta should not be nil, _YYModelPropertyMeta.
 @param _context      _context.model and _context.dictionary should not be nil.
 */
static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) {
    ModelSetContext *context = _context;
    __unsafe_unretained NSDictionary *dictionary = (__bridge NSDictionary *)(context->dictionary);
    __unsafe_unretained _YYModelPropertyMeta *propertyMeta = (__bridge _YYModelPropertyMeta *)(_propertyMeta);
    if (!propertyMeta->_setter) return;
    id value = nil;
    //判断是否为array类型的
    if (propertyMeta->_mappedToKeyArray) {
        value = YYValueForMultiKeys(dictionary, propertyMeta->_mappedToKeyArray);
    } else if (propertyMeta->_mappedToKeyPath) {
//判断是否为KeyPath类型的
        value = YYValueForKeyPath(dictionary, propertyMeta->_mappedToKeyPath);
    } else {
//这里是直接转换值
        value = [dictionary objectForKey:propertyMeta->_mappedToKey];
    }
    
    if (value) {
        __unsafe_unretained id model = (__bridge id)(context->model);
        ModelSetValueForProperty(model, value, propertyMeta);
    }
}


/// Get the value with multi key (or key path) from dictionary
/// The dic should be NSDictionary
static force_inline id YYValueForMultiKeys(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *multiKeys) {
    id value = nil;
// for遍历里面所有的key拿出来 相应的进行下次路由  这里分string类型 和 其他类型,其他类型继续进行路由
    for (NSString *key in multiKeys) {
        if ([key isKindOfClass:[NSString class]]) {
            value = dic[key];
            if (value) break;
        } else {
// 路由这里就要进行遍历,将嵌套的一层一层全部拿出来 
            value = YYValueForKeyPath(dic, (NSArray *)key);
            if (value) break;
        }
    }
    return value;
}

//最后走 ModelSetValueForProperty 方法  ,NSNumber NSString NSAttributedString各种各样的类型在里面都要发送msg_send 调用  meta->_setter 方法,对 (也就是发送setter 信息)
/**
 Set value to model with a property meta.
 
 @discussion Caller should hold strong reference to the parameters before this function returns.
 
 @param model Should not be nil.
 @param value Should not be nil, but can be NSNull.
 @param meta  Should not be nil, and meta->_setter should not be nil.
 */
static void ModelSetValueForProperty(__unsafe_unretained id model,
                                     __unsafe_unretained id value,
                                     __unsafe_unretained _YYModelPropertyMeta *meta) ;

8. category源码分析

如果给一个类添加一个分类,并重写这个类的某一个方法,问,到底会不会覆盖类里面的方法

// 遍历方法  可以通过这个方法来验证是否覆盖
-(void) printMethodNameOfClass:(Class)cls
{
    unsigned int count;
    //获取方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    //存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    //遍历所有方法
    for (int i = 0 ; i <count ; I++)
    {
        //获取方法
        Method method = methodList[I];
        //获取方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));  
        //拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@","];
    }
    
    //释放
    free(methodList);  
    // 打印方法名
    NSLog(@"%@ - %@",cls, methodNames);
}
  • 分类的方法是如何加载
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
//这里就是所有的img进行的镜像加载
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();
// 通过map_images 这个函数
//进入到 void map_images_nolock(unsigned mhCount, const char * const mhPaths[],const struct mach_header * const mhdrs[])
// 开始读所有的镜像文件
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

//读取函数
if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
//此处省略N多代码 

// 这里就会出来很多的类,包括子类等等都会加载过来
for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[I];
            Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);

            //类加载
            if (newCls != cls  &&  newCls) {
                // Class was moved but not deleted. Currently this occurs 
                // only when the new class resolved a future class.
                // Non-lazily realize the class below.
                resolvedFutureClasses = (Class *)
                    realloc(resolvedFutureClasses, 
                            (resolvedFutureClassCount+1) * sizeof(Class));
                resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
            }
        }

//方法编号的加载
static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        for (EACH_HEADER) {
            if (hi->isPreoptimized()) continue;
            
            bool isBundle = hi->isBundle();
            SEL *sels = _getObjc2SelectorRefs(hi, &count);
            UnfixedSelectors += count;
            for (i = 0; i < count; i++) {
                const char *name = sel_cname(sels[i]);
                sels[i] = sel_registerNameNoLock(name, isBundle);
            }
        }
    }

// Discover protocols. Fix up protocol refs.
//协议加载
    for (EACH_HEADER) {
        extern objc_class OBJC_CLASS_$_Protocol;
        Class cls = (Class)&OBJC_CLASS_$_Protocol;
        assert(cls);
        NXMapTable *protocol_map = protocols();
        bool isPreoptimized = hi->isPreoptimized();
        bool isBundle = hi->isBundle();
        // 最后其实就是一个列表 通过这个_getObjc2ProtocolList函数
  // 整个进程会维护这张表
        protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
        for (i = 0; i < count; i++) {
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
        }
    }

// Discover categories. 
// 探索重点~~~!!!
// category 加载 他也是通过这个_getObjc2CategoryList来加载的
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[I];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized())
                {
                    // remethodizeClass 函数加载
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }
}

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
//判断是否能找到这个category
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        //找到就attachCategories 把cats贴上去 
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

// 通过- 汇编lldb - 源码 - 官方文档 - 坑

一级标题

二级标题

五级标题
  • 列表第一项
  • 列表第二项
  1. 有序列表第一项
  2. 有序列表第二项

[图片上传失败...(image-12bdd7-1557046383947)]
斜体
粗体

引用段落

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

推荐阅读更多精彩内容

  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    我的梦工厂阅读 891评论 1 8
  • KVC(Key-valuecoding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS...
    榕樹頭阅读 702评论 0 2
  • KVC简单介绍 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key...
    公子无礼阅读 1,387评论 0 6
  • KVC是Key Value Coding的简称。它是一种可以通过字符串的名字(key)来访问类属性的机制。而不是通...
    153037c65b0c阅读 11,460评论 15 17
  • KVC是Key Value Coding的简称。它是一种可以通过字符串的名字(key)来访问类属性的机制。而不是通...
    _李恒阅读 742评论 0 0