你要知道的KVC、KVO、Delegate、Notification都在这里
转载请注明出处 http://www.jianshu.com/p/fa941b769606
本系列文章主要通过讲解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
//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 头文件
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 (
">",
">",
">",
">"
)
_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
@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
指针按照之前讲解的顺序去查找相关属性进行赋值和获取值的操作。如有读者清楚还请不吝赐教。
备注
由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。