Key-Value Coding
,俗称“键值编码”,可以通过一个key来访问某个属性。- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;//通过keyPath可以设置属性的属性
- (void)setValue:(id)value forKey:(NSString *)key;//通过key设置自己的属性
- (id)valueForKeyPath:(NSString *)keyPath;//通过keyPath访问属性的属性
- (id)valueForKey:(NSString *)key; //通过key访问自己的属性
setValue:forKey
的原理详解:
setKey:
方法(即对应key的set方法),找到了就去传递参数调用方法;_setKey:
的方法,找到可传值;+ (BOOL)accessInstanceVariablesDirectly;
这个方法是不是设置为YES(默认是YES),如果设置的是YES这表示直可以接访问实例成员变量;_key
的成员变量、有就设值;_isKey
的成员变量、有就设值;key
的成员变量、有就设值;isKey
的成员变量、有就设值;NSUnknownKeyException
accessInstanceVariablesDirectly
设置为NO,并且setKey:、_setKey:
这两个set方法没找到就会抛异常,不会去访问属性。示例1:
@interface Person : NSObject{
@public
// int _age;
int _isAge;
int age;
int isAge;
}
/**age属性*/
//@property(nonatomic,assign) int age;
@end
@implementation Person
//-(void)setAge:(int)age{
// _age = age;
//}
//-(void)_setAge:(int)age{
// _age = age;
//}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person * p = [[Person alloc]init];
[p setValue:@18 forKey:@"age"];
// NSLog(@"%d",p->age);
}
return 0;
}
上面代码和调试结果可以看出:当把两个set方法和_age
成员变量注销后会优先访问_isAge
成员变量,可以通过注销其他的成员变量一次证明方法的顺序,这里不再赘述。
在Person类中重写+ (BOOL)accessInstanceVariablesDirectly;
方法,并设置返回值为NO
,编译就会抛异常。
@implementation Person
+ (BOOL)accessInstanceVariablesDirectly{
return NO;
}
@end
getKey
、key
、isKey
、_key
,找到前面的方法就调用给成员变量取值就不会再往下找了。_key
、_isKey
、key
、iskey
找到前面的成员变量取值就不会再往下找了;NSUnknownKeyException
accessInstanceVariablesDirectly
设置为NO,并且上面的方法都没有找到也会抛异常。注意
:上面的赋值取值方法以及成员变量可以是私有的,依然可以访问,即KVC是可以访问对象的私有属性的。//.m文件
#import "Person.h"
@interface Person(){
@private
int _age;
int _isAge;
int age;
int isAge;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person * p = [[Person alloc]init];
[p setValue:@18 forKey:@"age"];
NSLog(@"%@",[p valueForKey:@"age"]);
}
return 0;
}
KVO
的全称是Key-Value Observing
,俗称“键值监听”,可以用于监听某个对象属性值的改变。- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
方法注册观察者,观察者可以接收keyPath
属性的变化事件。-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
方法,当keyPath
属性发生改变后,KVO会回调这个方法来通知观察者。- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
方法将KVO移除。需要注意的是,调用removeObserver
需要在观察者消失之前,否则会导致Crash
kvc
修改属性也是可以触发监听的。#import
@interface Person : NSObject
/**name*/
@property(nonatomic,copy) NSString * name ;
/**age*/
@property(nonatomic,assign) int age;
@end
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
/**person */
@property(nonatomic,strong) Person * person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc]init];
self.person.name = @"a";
self.person.age = 16;
//给person添加kvo监听,监听age和name属性值的变化
/*
observer:监听者
keyPath:属性对应的键
options:监听内容,一般是监听新值和旧值
context:可以传递一些参数
*/
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"参数1"];
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"参数2"];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person.name = @"aa";
self.person.age = 18;
}
// 当监听对象的属性值发生改变时,就会调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
//移除对应的监听者
-(void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"age"];
}
@end
使用的KVO监听对象时,内部方法调用顺序示例:
#import
@interface Person : NSObject
/**age*/
@property(nonatomic,assign) int age;
@end
@implementation Person
-(void)setAge:(int)age{
_age = age;
NSLog(@"setAge:");
}
-(void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"%@:willChangeValueForKey",key);
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"%@:didChangeValueForKey - begin",key);
[super didChangeValueForKey:key];
NSLog(@"%@:didChangeValueForKey - end",key);
}
@end
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
/**person */
@property(nonatomic,strong) Person * person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc]init];
self.person.age = 16;
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"参数2"];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person.age = 18;
}
// 当监听对象的属性值发生改变时,就会调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了- %@", object, keyPath, context);
}
-(void)dealloc{
[self.person removeObserver:self forKeyPath:@"age"];
}
@end
通过转换为的cpp代码时,我们可以发现使用的KVO监听对象时:
RuntimeAPI
动态生成一个子类NSKVONotifying_xxx
,让实例对象的isa
指向这个全新的子类,新生成的子类重写了setter/getter
方法,其中setter
方法中会调用Foundation
的_NSSetXXXValueAndNotify
函数;_NSSetXXXValueAndNotify
内部又调用了 willChangeValueForKey:
、父类原来的setter
、didChangeValueForKey:
;didChangeValueForKey:
方法时就会触发监听器调用-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
方法。#import "NSKVONotifying_MJPerson.h"
@implementation NSKVONotifying_MJPerson
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
// 屏幕内部实现,隐藏了NSKVONotifying_MJPerson类的存在
- (Class)class
{
return [MJPerson class];
}
- (void)dealloc
{
// 收尾工作
}
- (BOOL)_isKVOA
{
return YES;
}
@end
RuntimeAPI
动态生成一个子类,并且让instance
对象的isa
指向这个全新的子类;instance
对象的属性时,会调用Foundatio
n的_NSSetXXXValueAndNotify
函数、willChangeValueForKey:
、父类原来的setter
、didChangeValueForKey:
、Oberser
)的监听方法(observeValueForKeyPath:ofObject:change:context:
)willChangeValueForKey:和didChangeValueForKey:
,或者通过kvc
去修改属性。#import
//Person只有一个公开的成员变量
@interface Person : NSObject{
@public
int age;
}
@end
//PersonObserver是自定义的监听者
#import "PersonObserver.h"
@implementation PersonObserver
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"%@的%@值发送变化了!!!",object,keyPath);
}
@end
#import "ViewController.h"
#import "Person.h"
#import "PersonObserver.h"
@interface ViewController ()
/**person */
@property(nonatomic,strong) Person * person;
@property(nonatomic ,strong) PersonObserver * observer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc]init];
self.person -> age = 16;
self.observer = [[PersonObserver alloc]init];
[self.person addObserver:self.observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[self.person willChangeValueForKey:@"age"];
self.person -> age = 18;
// [self.person setValue:@18 forKey:@"age"];
[self.person didChangeValueForKey:@"age"];
}
-(void)dealloc{
[self.person removeObserver:self.observer forKeyPath:@"age"];
}
@end
结果可见:如果直接通过指针给属性赋值是无法触发监听方法的,但是要是手动调用Person
的willChangeValueForKey:
和didChangeValueForKey:
方法就会触发监听方法,或者通过kvc
也可以直接触发监听方法。
KVO原理分析及使用进阶