KVC的理解、应用及其底层原理

一、KVC的概念理解及常用方法

概念

KVC(Key-Value Coding)顾名思义,就是键值编码的意思。
iOS中,KVC就是通过使用属性的名称间接性来访问属性的方法,通俗一点的理解就是可以通过对象属性名称(Key)直接给属性值(Value)编码(Coding)“编码”可以理解为“赋值”。

这个方法可以不通过getter/setter方法来访问对象的属性。因为一个类的成员变量如果没有提供getter/setter的话,外界就失去了对这个变量的访问渠道。而KVC则提供了一种访问的方法,这个在某些场合会很有威力。例如,直接修改系统控件的内部属性,并且KVC还是KVOCoreData的技术基础,他们都是利用了OC的动态性。

KVC常用的方法
   - (void)setValue:(nullable id)value forKey:(NSString *)key; // 为对象的属性赋值
   - (id)valueForKey:(NSString *)key;  // 根据key取值
   - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  // 为对象的属性赋值(包含了setValue:forKey:的功能,并且还可以对对象内的类的属性进行赋值)
   - (nullable id)valueForKeyPath:(NSString *)keyPath; // 根据keyPath取值
   - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues; // 对模型一次性赋值,前提是必须声明好所有对应的属性(key)

现在我们用例子简单的使用上面提到的KVC常用到的方法
创建一个YGPerson和YGCar类,头文件分别为:

YGPerson.h
YGPerson.h
YGCar
YGCar.h
ViewController
ViewController.h

从上面三张图片的代码,我们分析一下:
YGPerson和YGCar两个类中,我们都是简单的声明了属性,但是YGPerson中,我们用@class引入了YGCar的头文件,将YGCar这个类的对象声明成我们的属性。而通过setValue: forKey:setVaule: forKeyPath:分别能给YGPerson中的name属性和YGCar中的price属性赋值。
并且通过打印结果,我们知道KVC通过value:forKey可以读取了属性的值,也进一步印证了我们开始的总结:KVC可以通过属性的名称间接的访问属性的方法,能赋值也能取值。

从下图来看,我们使用setValue: forKey:给price赋值时就会报
"[setValue:forUndefindKey:]:this is not key value coding-compliant for key car.price"的错误,意思是说在setValue:forUndefinKey 这方法中找不到car.price的属性。所以我们就能知道setValue:forKey此方法是在YGPerson类中无法找到car.price这个属性,而setValue:forKeyPath是通过.号来把一个一个key链接起来,这样就可以根据这个路径访问下去

image.png

通过上面的分析,KVC的注意点
1、在Person中我仅仅只是声明了@class Car,而没有引用#import "Car.h",然后在ViewController.m中便可以对其进行: [person setValue:[NSNumber numberWithFloat:price] forKeyPath:@"car.price"];这样子的赋值。所以说明KVC会去自动查找Car类进行赋值
2、-(void)setValue:(nullable id)value forKey:(NSString *)key;

你会发现value的值必须是id,也就是说不能传基本数据类型,必须是指针类型的变量,所以使用基本数据类型的时候,要装箱成为NSNumber类型。

3、key和keyPath的区别:

keyPath方法是集成了key的所有功能,也就是说对一个对象的一般属性进行赋值、取值,两个方法是通用的,都可以实现。但是对对象中的对象的属性进行赋值,只有keyPath能够实现

4、当key的值是没有定义的,valueForUndefinedKey:这个方法会被调用,如果你自己写了这个方法,key的值出错就会调用到这里来
5.、因为类key反复嵌套,所以有个keyPath的概念,keyPath就是用.号来把一个一个key链接起来,这样就可以根据这个路径访问下去
setValuesForKeysWithDictionary:的巧妙使用(字典转模型)
-(instancetype)initWithDict:(NSDictionary *)dict{
         if (self = [super init]) {
               [self setValuesForKeysWithDictionary:dict]; 
          } 
         return self;
}

注意点:

  • 字典转模型的时候,字典中的某一个key一定要在模型中有对应的属性
  • 如果一个模型中包含了另外的模型对象,是不能直接转化成功的。
  • 通过kvc转化模型中的模型,也是不能直接转化成功的
  • 底层还是调用了setValue: forKey:

二、KVC的应用

1、修改系统控件的内部属性(Runtime+KVC)

例如,界面要设计成这样


1

但我们平时做轮播图的效果是这样的,这显然不是我们的效果,但是要如何修改UIPageControl才能改成我们想要的横线的效果呢?


2

那我们试想一下,UIPageControl默认的图片就是一个圆点,那这个圆点是可能是一张图片,它的属性应该会有类型是UIImage这样的属性,那我们查看一下他的头文件,看是否存在此类型的属性。
3

从头文件中,我们找不到我们想要相关的UIImage或UIImageView这样的属性类型,那我们想深入的查看一下UIPageControl.m文件里的属性,可能就是在.m文件里的私有属性,那我们如何能看到.m文件的成员属性呢?这就要通过runtime遍历出UIPageControl所有属性(包括私有属性)。

/**
 *  运行时(runtime),获取所有成员变量
 */
- (void)getMemberVariables
{
    unsigned ivarCount;
    Ivar *ivars = class_copyIvarList([UIPageControl class], &ivarCount);
    
    for (int i = 0; i < ivarCount; i ++) {
        
        NSString *varibale = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
        
        NSLog(@"%@",varibale);
    }
}

我们通过此方法打印出UIPageControl的所有属性,发现有_pageImage和_currentPageImage 两个属性是符合我们修改当前pageControl的图片和默认pageControl的图片,如下我们就能修改了我们想要的UIPageControl的图片了。

4
通过KVC来修改
UIPageControl *pageControl = [[UIPageControl alloc ] init];
[pageControl setValue:[UIImage imageNamed:%@"nomal.png"] forKey:@"_pageImage"];
[pageControl setValue:[UIImage imageNamed:%@"selected.png"] forKey:@"_currentPageImage"]

2、在xib/Storyboard中,也可以使用KVC,下面是在xib中使用KVC把图片边框设置成圆角

6

3、在对模型转换时,会出现警告。

例如:我们要将下列字典数据转化为模型

{
     "id"    :"23"
     "name"  :"zhangrm"
     "age"   :"25"
     "like"  :"oc"
}

当我们在模型类中定义时:如下。我们发现id是oc中的关键字,会报警告⚠️

   @property (nonatomic, strong) NSString *id;
   @property (nonatomic, strong) NSString *name;
   @property (nonatomic, strong) NSString *age;
   @property (nonatomic, strong) NSString *like;

虽然可以使用以下方法,对模型中的成员变量进行统一设置,但是出现警告总归是不好的:

- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

既然这样,可以选择手动一个个去实现。但是这样在数据少的时候可以试试,在数据比较多时就不太现实了,程序的可扩展性也不好。
两种解决方法:

方法1.重写setValue:forKey:

setValuesForKeysWithDictionary:的底层是调用setValue:forKey:的,所以可以考虑重写这个方法,并且判断其key是id时,手动转换成模型的成员变量名,这里假设把id对应成以下属性:

@property (nonatomic, strong) NSString *ID;

有了对应的属性名后,就可以重写底层方法了

- (void)setValue:(id)value forKey:(NSString *)key
{
    if ([key isEqualToString:@"id"]) {
        [self setValue:value forKeyPath:@"ID"];
    }else{
        [super setValue:value forKey:key];
    }
}

这样,当使用setValuesForKeysWithDictionary:就不会出现模型中找不到对应的成员变量的错误了,但是必须注意,在使用setValuesForKeysWithDictionary,属性必须和字典中的key一一对应。

方法二:使用rumtime
+ (instancetype)objcWithDict:(NSDictionary *)dict mapDict:(NSDictionary *)mapDict
{
    id objc = [[self alloc] init];


    // 遍历模型中成员变量
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList(self, &outCount);

    for (int i = 0 ; i < count; i++) {
        Ivar ivar = ivars[i];

        // 成员变量名称
        NSString *ivarName = @(ivar_getName(ivar));

        // 获取出来的是`_`开头的成员变量名,需要截取`_`之后的字符串
        ivarName = [ivarName substringFromIndex:1];

        id value = dict[ivarName];
        // 由外界通知内部,模型中成员变量名对应字典里面的哪个key
        // ID -> id
        if (value == nil) {
            if (mapDict) {
                NSString *keyName = mapDict[ivarName];

                value = dict[keyName];
            }
        }
        [objc setValue:value forKeyPath:ivarName];
    }
    return objc;
}

使用方法

+ (instancetype)itemWithDict:(NSDictionary *)dict
{
    // 传入key和实例变量名的映射字典@{@"ID":@"id"}
    TPCItem *item = [TPCItem objcWithDict:dict mapDict:@{@"ID":@"id"}];

    return item;
}

KVC底层原理分析

setValue:forKey:赋值原理如下:

  • 去模型中查找有没有对应的setter方法:例如:setIcon方法,有就直接调用这个setter方法给模型这个属性赋值[self setIcon:dic[@"icon"]];
  • 如果找不到setter方法,接着就会去寻找有没有icon属性,如果有,就直接访问模型中的icon属性,进行赋值,icon=dict[@"icon"];
  • 如果找不到icon属性,接着又会去寻找_icon属性,如果有,直接进行赋值_icon=dict[@"icon"];
  • 如果都找不到就会报错:[<Flag 0X7fb74bc7a2c0> setValue:forUndefinedKey:]
  • 如果对某个类,不允许使用KVC,可以通过设置 accessInstanceVariablesDirectly 控制。

KVC内部的实现

比如说如下的一行KVC的代码:

[site setValue:@"sitename" forKey:@"name"];

就会被编译器处理成:

SEL sel = sel_get_uid ("setValue:forKey:");

IMP method = objc_msg_lookup (site->isa,sel);

method(site, sel, @"sitename", @"name");

这下KVC内部的实现就很清楚的清楚了:一个对象在调用setValue的时候,(1)首先根据方法名找到运行方法的时候所需要的环境参数。(2)他会从自己isa指针结合环境参数,找到具体的方法实现的接口。(3)再直接查找得来的具体的方法实现。

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

推荐阅读更多精彩内容