KVC 使用方法详解及底层实现

你要知道的KVC、KVO、Delegate、Notification都在这里

转载请注明出处 http://www.jianshu.com/p/fa941b769606

本系列文章主要通过讲解KVC、KVO、Delegate、Notification的使用方法,来探讨KVO、Delegate、Notification的区别以及相关使用场景,本系列文章将分一下几篇文章进行讲解,读者可按需查阅。

KVC使用方法详解与底层实现

KVC(key value coding)键值编码是一种可以使用字符串形式来间接操作对象相关属性的方法。KVC需要由类别Category NSKeyValueCoding来支持,OC在实现KVC时没有采用实现接口的方式,而是针对NSObject创建了一个类别,通过这样的方式使得NSObject的子类可以自行实现NSKeyValueCoding类别定义的相关方法。

KVC使用非常简单,但KVC却异常强大,最暗黑的功能就是它可以无视访问限制,无论是否为private都可以进行赋值或取值操作,readonly的属性也可以无视,提供了一种比runtime更便捷的方式来修改或访问系统级隐藏的属性,因此,经常在开发中通过runtime获取相关属性名后使用KVC来修改那些只读readonly或隐藏的属性。

KVC基础方法详解

KVC常用方法主要由如下几个:

//获取属性名为key的属性的值
- (nullable id)valueForKey:(NSString *)key;

//设置属性名为key的属性的值为value
- (void)setValue:(nullable id)value forKey:(NSString *)key;

/*
提供一种类似于Java ONGL语法来访问嵌套属性
获取嵌套属性名为keyPath的属性的值
*/
- (nullable id)valueForKeyPath:(NSString *)keyPath;

//设置嵌套属性名为keyPath的属性的值为value
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

/*
获取属性名为key的属性值时,如果属性不存在则执行该方法,可自定义实现,
默认实现方式为抛出NSUnknownKeyException异常
*/
- (nullable id)valueForUndefinedKey:(NSString *)key;

/*
设置属性名为key的属性值为value时,如果属性不存在则执行该方法,可自定义实现,
默认实现方式为抛出NSUnknownKeyException异常
*/
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

针对上述方法举一个栗子:

//Phone类
@interface Phone : NSObject
@property (nonatomic, strong) NSString *phoneNumber;
@end

@implementation Phone
@synthesize phoneNumber = _phoneNumber;
@end

//Person类
@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
//组合一个Phone的对象
@property (nonatomic, strong) Phone *phone;
- (void)showMyself;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;
@synthesize phone = _phone;

- (void)showMyself {
    NSLog(@"My name is %@ I am %ld years old. my phone number is %@", self.name, self.age, self.phone.phoneNumber);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        
        [p setValue:@"Jiaming Chen" forKey:@"name"];
        [p setValue:@22 forKey:@"age"];
        [p setValue:[[Phone alloc] init] forKey:@"phone"];
        [p setValue:@"18666668888" forKeyPath:@"phone.phoneNumber"];
        //输出: My name is Jiaming Chen I am 22 years old. my phone number is 18666668888
        [p showMyself];
        //输出: Name: Jiaming Chen
        NSLog(@"Name: %@", [p valueForKey:@"name"]);
        //输出: Age: 22
        NSLog(@"Age: %@", [p valueForKey:@"age"]);
        //输出: Phone Number: 18666668888
        NSLog(@"Phone Number: %@", [p valueForKeyPath:@"phone.phoneNumber"]);
    }
    return 0;
}

上面的栗子使用了setValue:forKeyvalueForKey:setValue:forKeyPathvalueForKeyPath方法。Person类组合了Phone类,因此在访问phone属性phoneNumber属性时,需要使用keyPath这样的字符串点语法,可以根据实际情况一直嵌套下去。这个栗子比较简单,不做过多赘述。接下来在看一个栗子:

@interface Person : NSObject
{
    @private
    NSString *name;
    NSString *_name;
}

- (void)outputAddress;

@end

@implementation Person
{
    NSInteger age;
}

- (void)outputAddress
{
    NSLog(@"Address name: %p _name: %p", name, _name);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        
        [p setValue:@"Jiaming Chen" forKey:@"name"];
        [p setValue:@"CCCC" forKey:@"_name"];
        [p setValue:@22 forKey:@"age"];
        
        //输出: Name: CCCC 0x1000010a8
        NSLog(@"Name: %@ %p", [p valueForKey:@"name"], [p valueForKey:@"name"]);
        //输出: _Name: CCCC 0x1000010a8
        NSLog(@"_Name: %@ %p", [p valueForKey:@"_name"], [p valueForKey:@"_name"]);
        //输出: Age: 22
        NSLog(@"Age: %@", [p valueForKey:@"age"]);
        //输出: Address name: 0x0 _name: 0x1000010a8
        [p outputAddress];

    }
    return 0;
}

为了展示实验效果这里没有使用合成存取方法,Person类声明的属性name_name以及age都是private的,但是KVC依旧可以为其设置值,同样的也可以获取private属性的值,这就是KVC的强大之处。

但似乎上面栗子的输出结果与我们预期不同,明明通过setValue:forKey:name属性设置的值是Jiaming Chen但通过valueForKey:输出的结果却与_name属性值一致,连输出的地址都一样。通过outputAddress方法输出name_name的地址后发现name的地址为0x0,这表示其并未初始化,出现这种情况的原因正是因为KVC获取值和赋值的顺序有关,由于篇幅问题,这里没有给出所有的实验过程,有兴趣的读者可以按照下述顺序自行实验,通过实验可得如下赋值顺序:

  • 首先通过setter方法即set(Key属性名):,这里是setName:方法进行赋值。
  • 如果没有setter方法,寻找_(key属性名),这里是_name成员变量,无视该成员变量的访问修饰符,也无视该成员变量是在@interface的类接口部分定义的还是在@implementation类实现部分定义的,只要存在该名称的成员变量就为其赋值。
  • 如果没有setter方法也没有_(key属性名),这里是_name成员变量,就会寻找key属性名,这里是name成员变量,同样无视其访问修饰符,无视其定义位置,只要存在该名称的成员变量就为其赋值。
  • 如果setter_(key属性名)key属性名都不存在则会调用setValue:forUndefinedKey:方法,该方法默认实现是抛出NSUnknownKeyException异常。

同样的,对于valueForKey:方法来获取值的顺序如下:

  • 首先通过getter方法来获取值,这里为name方法。
  • 如果没有getter方法则会查找名称为_(key属性名)这里为_name的成员变量,同样无视访问修饰符,无视定义位置,只要存在该成员变量就返回其值。
  • 如果没有getter方法也没有_(key属性名)成员变量,则查找名称为key属性值这里为name的成员变量,同样无视访问修饰符,无视定义位置,只要存在该成员变量就返回其值。
  • 如果getter_(key属性名)key属性名都不存在则会调用valueForKey方法,该方法默认实现是抛出NSUnknownKeyException方法。

当我们清楚的认识到上述KVC获取值和赋值的相关顺序后,也就理解了前一个栗子结果产生的原因,通过上面的讲解也可以发现其实KVC方法的效率并不高,KVC还是要去搜索gettersetter搜索各种成员变量,显然通过直接赋值或获取值效率更高,所以,在普通情况下尽量不要使用KVC这样的方式。

接下来再举一个在实际开发中常使用的栗子:

#import <Foundation/Foundation.h>
//Person类
@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
//服务端为id,由于id是OC的关键字,取名为idNumber
@property (nonatomic, copy) NSString *idNumber;
- (void)showMyself;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;
@synthesize idNumber = _idNumber;

- (void)showMyself {
    NSLog(@"Name: %@ Age: %ld idNumber: %@", self.name, self.age, self.idNumber);
}

- (nullable id)valueForUndefinedKey:(NSString *)key
{
    return nil;
}

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key
{
    //如果这个key为id
    if ([key isEqualToString:@"id"])
    {
        //调用setValue:forKey方法为idNumber赋值
        [self setValue:value forKey:@"idNumber"];
    }
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //假设为服务端获取的json数据转换的dictionary
        NSDictionary *dict = @{@"name": @"Jiaming Chen", @"age": @20, @"id": @"1603121434"};
        
        Person *p = [[Person alloc] init];
        //遍历上述字典的key
        [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            //直接使用kvc赋值,不需要再写一行一行代码赋值
            [p setValue:obj forKey:key];
        }];
        //输出: Name: Jiaming Chen Age: 20 idNumber: 1603121434
        [p showMyself];
    }
    return 0;
}

上面的栗子在Person类中自定义实现了valueForUndefinedKey:setValue:forUndefinedKey:方法,如果不实现该方法设置不存在的key时默认抛出异常,在实际开发中通常需要从服务端获取大量的json数据,转换为字典后往往需要一个属性一个属性的赋值,使用KVC方法就能够避免编写冗长的代码,但有时服务端和客服端的数据名称会有不同,此时可以按情况在setValue:forUndefinedKey:方法中进行处理。

在实际开发中还遇到过一种情况,iOS端的对象使用NSString类型存储用户ID,但服务端返回的是int类型的数据,在赋值时就会崩溃,解决该问题需要我们自己实现setValue:forKey:方法,在该方法中判断value的类型后手动转换即可,在此不再赘述。

通过上面的栗子,如果需要使用KVC进行赋值操作,最好按照需求自定义实现valueForUndefinedKey:setValue:forUndefinedKey:以及setValue:forKey:方法。

KVC修改readonly的系统隐藏变量

首先上一张阿里云iOS端app的图,如下图所示:

阿里云iOS端首页

我们发现首页上方旋转木马的UIPageControl不是传统的圆形而是长条形,如果不使用自定义控件或是使用h5实现,那我们该如何实现这个效果呢?

首先我们使用如下代码创建一个UIPageControl:

- (instancetype)init
{
    if (self=  [super init])
    {
        self.view.backgroundColor = [UIColor whiteColor];
        
        UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 200, ScreenWidth, 200)];
        containerView.backgroundColor = [UIColor greenColor];
        [self.view addSubview:containerView];
        
        UIPageControl *pageControler = [[UIPageControl alloc] initWithFrame:CGRectMake(0, 180, ScreenWidth, 20)];
        pageControler.numberOfPages = 4;
        [pageControler setPageIndicatorTintColor:[UIColor blueColor]];
        [pageControler setCurrentPageIndicatorTintColor:[UIColor blackColor]];
        [containerView addSubview:pageControler];       
    }
    return self;
}

实现效果如下图:

UIPageControl基本样式

首先查看UIPageControl提供给我们可访问的属性,看一下有没有可以操作的属性,这里可以自行查看,我们发现并没有这样的属性存在,这个时候该怎么办呢?接着我们可以使用runtimeUIPageControl的所有属性都打印出来,runtime的强大之处就在于可以获取类的任意属性和方法,关于runtime部分本博客有一系列文章来讲解,有兴趣的读者可以自行查阅iOS runtime探究(一): 从runtime开始理解面向对象的类到面向过程的结构体

我们先打印出UIPageControl所有属性,看一下有没有我们需要的,代码如下:

执行下述代码需要import <objc/runtime.h>头文件

unsigned int count = 0;
//该方法是C函数,获取所有属性
Ivar * ivars = class_copyIvarList([pageControler class], &count);
for (unsigned int i = 0; i < count; i ++) 
{
    Ivar ivar = ivars[i];
    //获取属性名
    const char * name = ivar_getName(ivar);
    //使用KVC直接获取相关属性的值
    NSObject *value = [pageControler valueForKey:[NSString stringWithUTF8String:name]];
    NSLog(@"%s %@", name, value);
}
//需要释放获取到的属性
free(ivars);

输出如下:

_lastUserInterfaceIdiom -1
_indicators (
    "<UIView: 0x100b0d820; frame = (-3.5 -3.5; 7 7); layer = <CALayer: 0x1c4227c00>>",
    "<UIView: 0x100b0da00; frame = (-3.5 -3.5; 7 7); layer = <CALayer: 0x1c4227cc0>>",
    "<UIView: 0x100b0dbe0; frame = (-3.5 -3.5; 7 7); layer = <CALayer: 0x1c4227d20>>",
    "<UIView: 0x100b0ddc0; frame = (-3.5 -3.5; 7 7); layer = <CALayer: 0x1c4227da0>>"
)
_currentPage 0
_displayedPage 0
_pageControlFlags (null)
_currentPageImage (null)
_pageImage (null)
_currentPageImages (null)
_pageImages (null)
_backgroundVisualEffectView (null)
_currentPageIndicatorTintColor UIExtendedGrayColorSpace 0 1
_pageIndicatorTintColor UIExtendedSRGBColorSpace 0 0 1 1
_legibilitySettings (null)
_numberOfPages 4        

从属性名我们发现了几个比较重要的属性_currentPageImage_pageImage_currentPageImages_pageImages,通过属性名称可以判断这些就是我们要找的属性,接着使用KVC为其设置我们自己的图片,代码如下:

[pageControler setValue:[UIImage imageNamed:@"line"] forKeyPath:@"pageImage"];
[pageControler setValue:[UIImage imageNamed:@"current"] forKeyPath:@"currentPageImage"];

实现效果如下:

修改后的效果

在我们需要修改系统提供UI界面而又束手无策时可以使用runtime获取属性来查看是否有可以使用的属性或方法,接着可以使用KVC获取相关值或进行赋值操作,这种方法可能也会存在风险,如果获取的是苹果禁用的私有API那就只能乖乖想别的方法了,不过KVC提供了一种修改系统实现的思路。

KVC底层实现

首先,继续第一个栗子,我们实现如下代码:

#import <Foundation/Foundation.h>

@interface Person : NSObject
//为了方便查看重写的代码将name改成cjmName
@property (nonatomic, copy) NSString *cjmName;
@property (nonatomic, assign) NSUInteger age;
- (void)showMyself;

@end

@implementation Person

@synthesize cjmName = _cjmName;
@synthesize age = _age;

- (void)showMyself {
    NSLog(@"Name: %@ Age: %ld", self.cjmName, self.age);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        
        [p setValue:@"Jiaming Chen" forKey:@"cjmName"];
        [p setValue:@22 forKey:@"age"];
        
        p.cjmName = @"CCCC";
        
        [p showMyself];
    }
    return 0;
}

接着使用clang -rewrite-objc main.m重写为cpp文件,查看main函数重写后的代码如下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

        Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));

        ((void (*)(id, SEL, id _Nullable, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setValue:forKey:"), (id _Nullable)(NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_080287_mi_1, (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_080287_mi_2);
        ((void (*)(id, SEL, id _Nullable, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setValue:forKey:"), (id _Nullable)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 22), (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_080287_mi_3);

        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setCjmName:"), (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_080287_mi_4);

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("showMyself"));
    }
    return 0;
}

通过上面的重写代码似乎没有什么特别的发现,对于setValue:forKey:方法的调用与普通方法相同,所以,这里猜测底层实现可能是在执行KVC相关方法时,在继承树上沿着isa指针按照之前讲解的顺序去查找相关属性进行赋值和获取值的操作。如有读者清楚还请不吝赐教。

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。

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

推荐阅读更多精彩内容

  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    黑暗中的孤影阅读 49,331评论 74 439
  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    Fendouzhe阅读 488评论 0 6
  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    朽木自雕也阅读 1,394评论 6 1
  • KVC简单介绍 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key...
    公子无礼阅读 1,179评论 0 6
  • 简介 KVC(Key-value coding)键值编码,翻译一下就是指iOS的开发中,可以允许开发者通过Key名...
    6ffd6634d577阅读 1,134评论 1 9