您的位置:首页 > 移动开发 > IOS开发

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

2017-10-13 13:12 423 查看

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

转载请注明出处 http://blog.csdn.net/u014205968/article/details/78224815

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

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

KVO 正确使用姿势进阶及底层实现

Protocol与Delegate 使用方法详解

NSNotificationCenter 通知使用方法详解

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:forKey
valueForKey:
setValue:forKeyPath
valueForKeyPath
方法。
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
还是要去搜索
getter
setter
搜索各种成员变量,显然通过直接赋值或获取值效率更高,所以,在普通情况下尽量不要使用
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的图,如下图所示:



我们发现首页上方旋转木马的
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
提供给我们可访问的属性,看一下有没有可以操作的属性,这里可以自行查看,我们发现并没有这样的属性存在,这个时候该怎么办呢?接着我们可以使用
runtime
UIPageControl
的所有属性都打印出来,
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
指针按照之前讲解的顺序去查找相关属性进行赋值和获取值的操作。如有读者清楚还请不吝赐教。

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  ios-kvc delegate
相关文章推荐