KVC是Cocoa的一个大招,最主要的用处就是可以随意的修改一个对象的属性和成员变量,并且私有的也可以修改
forKeyPath包含了forKey的功能,以后使用forKeyPath就可以了
forKeyPath中可以利用.运算符, 就可以一层一层往下查找对象的属性
Person *p = [[Person alloc] init];
p.dog = [[Dog alloc] init];
p.dog.bone = [[Bone alloc] init];
// p.dog.bone.type = @"狗骨";
[p setValue:@"猪骨" forKeyPath:@"dog.bone.type"];
[p.dog setValue:@"猪骨" forKeyPath:@"bone.type"];
[p.dog.bone setValue:@"猪骨" forKeyPath:@"type"];
//也可以传入一个字典实现给模型赋值
[self setValuesForKeysWithDictionary:dict];
//[p setValue:@"hashiqi" forKey:@"dog.name"]; // 写法错误
协议定义介绍:
KVC全称是Key Value Coding,定义在NSKeyValueCoding.h文件中,是一个非正式协议。KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量。
NSKeyValueCoding Protocol
在NSKeyValueCoding中提供了KVC通用的访问方法,分别是getter方法valueForKey:和setter方法setValue:forKey:,以及其衍生的keyPath方法,这两个方法各个类通用的。并且由KVC提供默认的实现,我们也可以自己重写对应的方法来改变实现。
一.KVC基本操作
①基础操作
KVC主要对三种类型进行操作,基础数据类型及常量、对象类型、集合类型。
@interface BankAccount : NSObject
@property (nonatomic, strong) NSNumber *currentBalance;
@property (nonatomic, strong) Person *owner;
@property (nonatomic, strong) NSArray
*transactions; @end
在使用KVC时,直接将属性名当做key,并设置value,即可对属性进行赋值。
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
②keyPath
除了对当前对象的属性进行赋值外,还可以对其更“深层”的对象进行赋值。例如对当前对象的address属性的street属性进行赋值。KVC进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。
[myAccount setValue:@"中关村大街" forKeyPath:@"address.street"];
通过keyPath对数组进行取值时,并且数组中存储的对象类型都相同,可以通过valueForKeyPath:方法指定取出数组中所有对象的某个字段。例如下面例子中,通过valueForKeyPath:将数组中所有对象的name属性值取出,并放入一个数组中返回。
NSArray *names = [array valueForKeyPath:@"name"];
③多值操作
需要注意的是,虽然看到dictionary的字样,下面两个方法并不是字典的方法。
KVC还有更强大的功能,可以根据给定的一组key,获取到一组value,并且以字典的形式返回,获取到字典后可以通过key从字典中获取到value。
- (NSDictionary
*)dictionaryWithValuesForKeys:(NSArray *)keys;
同样,也可以通过KVC进行批量赋值。在对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含key、value的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给User对象的属性赋值。
- (void)setValuesForKeysWithDictionary:(NSDictionary
*)keyedValues;
④实用技巧
在项目中经常会遇到字典转模型的情况,如果在自定义的init方法里逐个赋值,这样每次数据发生改变还需要改赋值语句。然而通过KVC为我们提供的赋值API,可以对数据进行批量赋值。假设有以下JSON数据并定义User类,在外界通过setValuesForKeysWithDictionary:方法对User进行赋值。
JSON数据:
{
"username": "lxz",
"age": 25,
"id": 100
}
@interface User : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSString age;
@property (nonatomic, assign) NSInteger userId;
@end
@implementation User
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if ([key isEqualToString:@"id"]) {
self.userId = [value integerValue];
}
}
@end
赋值时会遇到一些问题,例如服务器会返回一个id字段,但是对于客户端来说id是系统保留字段,可以重写setValue:forUndefinedKey:方法并在内部处理id参数的赋值。
转换时需要服务器数据和类定义匹配,字段数量和字段名都应该匹配。如果User比服务器数据多,则服务器没传的字段为空。如果服务端传递的数据User中没有定义,则会导致崩溃。
在KVC进行属性赋值时,内部会对基础数据类型做处理,不需要手动做NSNumber的转换。需要注意的是,NSArray和NSDictionary等集合对象,value都不能是nil,否则会导致Crash。
⑤异常信息
当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个NSUndefinedKeyException的异常,并且应用程序Crash。
我们可以重写下面两个方法,根据业务需求合理的处理KVC导致的异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
当通过KVC给某个非对象的属性赋值为nil时,此时KVC会调用属性所属对象的setNilValueForKey:方法,并抛出NSInvalidArgumentException的异常,并使应用程序Crash。
我们可以通过重写下面方法,在发生这种异常时进行处理。例如给name赋值为nil的时候,就可以重写setNilValueForKey:方法并表示name是空的。
- (void)setNilValueForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
[self setValue:@"" forKey:@”age”];
} else {
[super setNilValueForKey:key];
}
}
二.KVC集合操作
①集合属性操作
根据KVO的实现原理,是在运行时生成新的子类并重写其setter方法,在其内容发生改变时发送消息。但这只是对属性直接进行赋值会触发,如果属性是容器对象,对容器对象进行add或remove操作,则不会调用KVO的方法。可以通过KVC对应的API来配合使用,使容器对象内部发生改变时也能触发KVO。
在进行容器对象操作时,先调用下面方法通过key或者keyPath获取集合对象,然后再对容器对象进行add或remove等操作时,就会触发KVO的消息通知了。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
keyPath方法:
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
②集合运算符
KVC提供的valueForKeyPath:方法非常强大,可以通过该方法对集合对象进行“深入”操作,在其keyPath中嵌套集合运算符,例如求一个数组中对象某个属性的count。(集合对象主要指NSArray和NSSet,但不包括NSDictionary)
集合运算符格式
上面表达式主要分为三部分,left部分是要操作的集合对象,如果调用KVC的对象本来就是集合对象,则left可以为空。中间部分是表达式,表达式一般以@符号开头。后面是进行运算的属性。
集合运算符主要分为三类:
集合操作符:处理集合包含的对象,并根据操作符的不同返回不同的类型,返回值以NSNumber为主。
数组操作符:根据操作符的条件,将符合条件的对象包含在数组中返回。
嵌套操作符:处理集合对象中嵌套其他集合对象的情况,返回结果也是一个集合对象。
<一>集合操作符
示例代码如下:
下面是为了方便模拟KVC操作,而创建的测试代码。定义Transaction类为模型类,类中包含三种类型的属性。并定义BankAccount类,其中包含一个数组,下面的代码示例就都是操作这个数组的,并且数组包含所有Transaction对象。
@interface Transaction : NSObject
@property (nonatomic, strong) NSString *payee;
@property (nonatomic, strong) NSNumber *amount;
@property (nonatomic, strong) NSDate *date;
@end
@interface BankAccount : NSObject
@property (nonatomic, strong) NSArray *transactions;
@end
集合操作符处理NSArray和NSSet及其子类这样的集合对象,并根据不同的操作符返回不同类型的对象,返回值一般都是NSNumber。
@avg用来计算集合中right keyPath指定的属性的平均值。
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
@count用来计算集合的总数。
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
备注:@count操作符比较特殊,它不需要写right keyPath,即使写了也会被忽略。
@sum用来计算集合中right keyPath指定的属性的总和。
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
@max用来查找集合中right keyPath指定的属性的最大值。
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
@min用来查找集合中right keyPath指定的属性的最小值。
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
备注:@max和@min在进行判断时,都是通过调用compare:方法进行判断,所以可以通过重写该方法对判断过程进行控制。
<二>数组操作符
@unionOfObjects将集合对象中,所有payee对象放在一个数组中并返回。
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
@distinctUnionOfObjects将集合对象中,所有payee对象放在一个数组中,并将数组进行去重后返回。
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
注意:以上两个方法中,如果操作的属性为nil,在添加到数组中时会导致Crash。
<三>嵌套操作符
由于嵌套操作符是需要对嵌套的集合对象进行操作,所以新建一个arrayOfArrays对象,其中包含两个数组,数组中存储的都是Transaction类型对象。
NSArray *moreTransactions = ....;
NSArray *arrayOfArrays = @[self.transactions, moreTransactions];
@unionOfArrays是用来操作集合内部的集合对象,将所有right keyPath对应的对象放在一个数组中返回。
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
@distinctUnionOfArrays是用来操作集合内部的集合对象,将所有right keyPath对应的对象放在一个数组中,并进行排重。
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
@distinctUnionOfSets是用来操作集合内部的集合对象,将所有right keyPath对应的对象放在一个set中,并进行排重。
NSSet *collectedPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfSets.payee"];
小技巧
如果在集合对象中操作的属性,本来就是NSNumber类型,则可以像下面这样,直接用self代表值自身。
NSArray *array = @[@(productA.price), @(productB.price), @(productC.price), @(productD.price)];
NSNumber *avg = [array valueForKeyPath:@"@avg.self"];
非对象值处理
KVC是支持基础数据类型和结构体的,可以在setter和getter的时候,通过NSValue和NSNumber来转换为OC对象。Swift中不存在这样的需求,因为Swift中所有变量都是对象。
以下是结构体转换的示例代码,可以调用initWithBool:方法对基础数据类型进行包装,除了调用方法外还可以通过字面量实现,例如@(YES)的调用。通过NSNumber的boolValue属性转换为基础数据类型。
@property (nonatomic, assign, readonly) BOOL boolValue;
- (NSNumber *)initWithBool:(BOOL)value NS_DESIGNATED_INITIALIZER;
结构体转换的代码定义在UIGeometry.h中,以NSValue的Category形式存在。NSValue对CGPoint、CGRect等结构体都提供了转换方法,例如下面是对CGPoint进行转换的示例代码。
@property(nonatomic, assign, readonly) CGPoint CGPointValue;
+ (NSValue *)valueWithCGPoint:(CGPoint)point;
需要注意的是,无论什么时候都不应该给setter中传入nil,会导致Crash并引起NSInvalidArgumentException异常。
三.属性验证
①.基础属性验证
在调用KVC时可以先进行验证,验证通过下面两个方法进行,支持key和keyPath两种方式。验证方法默认实现返回YES,可以通过重写对应的方法修改验证逻辑。
验证方法需要我们手动调用,并不会在进行KVC的过程中自动调用。
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
下面是使用验证方法的例子。在validateValue方法的内部实现中,如果传入的value或key有问题,可以通过返回NO来表示错误,并设置NSError对象。
Person *person = [[Person alloc] init];
NSError *error;
NSString *name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
NSLog(@"%@", error);
}
②单独属性验证
KVC还支持对单独属性做验证,可以通过定义validate
- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) {
if (outError != NULL) {
*outError = [NSError errorWithDomain:PersonErrorDomain
code:PersonInvalidNameCode
userInfo:@{ NSLocalizedDescriptionKey
: @"Name too short" }];
}
return NO;
}
return YES;
}
我觉得KVC应该支持validateValue自动验证,在调用setValue或getValue时自动进行验证,如果不符合验证规则,就调用失败。如果外界使用的地方都先调用一次validateValue的话,这是很麻烦的。当然也有解决方法,可以通过Method Swizzling方法hook住setValue和getValue方法。
四.搜索规则
KVC在通过key或者keyPath进行操作的时候,可以查找属性方法、成员变量等,查找的时候可以兼容多种命名。具体的查找规则要以官方文档为主,所以我把官方文档翻译了一下写在下面。
在KVC的实现中,依赖setter和getter的方法实现,所以方法命名应该符合苹果要求的规范,否则会导致KVC失败。
在学习KVC的搜索规则前,要先弄明白一个属性的作用,这个属性在搜索过程中起到很重要的作用。这个属性表示是否允许读取实例变量的值,如果为YES则在KVC查找的过程中,从内存中读取属性实例变量的值。
@property (class, readonly) BOOL accessInstanceVariablesDirectly;
①基础Getter搜索模式
这是valueForKey:的默认实现,给定一个key当做输入参数,开始下面的步骤,在这个接收valueForKey:方法调用的类内部进行操作。
通过getter方法搜索实例,例如get
如果没有找到简单的getter方法,则搜索其匹配模式的方法countOf
如果找到其中的第一个和其他两个中的一个,则创建一个集合代理对象,该对象响应所有NSArray的方法并返回该对象。否则,继续到第三步。
代理对象随后将NSArray接收到的countOf
当代理对象和KVC调用方通过上面方法一起工作时,就会允许其行为类似于NSArray一样。
如果没有找到NSArray简单存取方法,或者NSArray存取方法组。则查找有没有countOf
如果找到三个方法,则创建一个集合代理对象,该对象响应所有NSSet方法并返回。否则,继续执行第四步。
此代理对象随后转换countOf
如果没有发现简单getter方法,或集合存取方法组,以及接收类方法accessInstanceVariablesDirectly是返回YES的。搜索一个名为_
如果发现对应的实例,则立刻获得实例可用的值并跳转到第五步,否则,跳转到第六步。
如果取回的是一个对象指针,则直接返回这个结果。
如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。
如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。
如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法。
②基础Setter搜索模式
这是setValue:forKey:的默认实现,给定输入参数value和key。试图在接收调用对象的内部,设置属性名为key的value,通过下面的步骤:
查找set
如果没有发现一个简单的setter,但是accessInstanceVariablesDirectly类属性返回YES,则查找一个命名规则为_
如果没有发现setter或实例变量,则调用setValue:forUndefinedKey:方法,并默认提出一个异常,但是一个NSObject的子类可以提出合适的行为。
③NSMutableArray搜索模式
这是mutableArrayValueForKey:的默认实现,给一个key当做输入参数。在接收访问器调用的对象中,返回一个名为key的可变代理数组,这个代理数组就是用来响应外界KVO的对象,通过下面的步骤进行查找:
查找一对方法insertObject:in
如果找到最少一个insert方法和最少一个remove方法,则返回一个代理对象,来响应发送给NSMutableArray的组合消息insertObject:in
当对象接收一个mutableArrayValueForKey:消息并实现可选替换方法,例如replaceObjectIn
如果对象没有可变数组方法,查找一个替代方法,命名格式为set
提示:
这一步描述的机制远不如上一步有效,因为它可能重复创建新的集合对象,而不是修改现有的对象。因此,在自己设计的KVC时应该尽量避免它。
如果没有可变数组的方法,也没有找到访问器,但接受响应的类accessInstanceVariablesDirectly属性返回YES,则查找一个名为_
按照这个顺序,如果找到实例变量,则返回一个代理对象。改对象将接收所有NSMutableArray发送过来的消息,通常是NSMutableArray或其子类。
如果所有情况都失败,则返回一个可变的集合代理对象。当它接收NSMutableArray消息时,发送一个setValue:forUndefinedKey:消息给接收mutableArrayValueForKey:消息的原始对象。
这个setValue:forUndefinedKey:的默认实现是提出一个NSUndefinedKeyException异常,但是子类可以重写这个实现。
④其他搜索模式
还有NSMutableSet和NSMutableOrderedSet两种搜索模式,这两种搜索模式和NSMutableArray步骤相同,只是搜索和调用的方法不同。详细的搜索方法都可以在KVC官方文档中找到,再套用上面的流程即可理解。
代码示例:
根据上面KVC查找规则的描述,我们定义一个TestObject类,并指定其他setter和getter,以及合成为其他的成员变量,看KVC是否能够找到属性的对象并赋值。
@interface TestObject : NSObject {
NSObject *_newObject;
}
@property (nonatomic, strong, setter=newSetObject:, getter=newObject) NSObject *object;
@property (nonatomic, strong) NSObject *twoObject;
@end
@implementation TestObject
@synthesize object = _newObject;
@end
这里对两个属性进行赋值,twoObject属性赋值没有任何问题,而第二个属性赋值则会导致Crash。崩溃信息如上面所述抛出一个NSUnknownKeyException异常,并提示没有找到object获取方法和实例对象。
TestObject *object = [[TestObject alloc] init];
[object setValue:[NSObject new] forKey:NSStringFromSelector(@selector(twoObject))];
[object setValue:[NSObject new] forKey:NSStringFromSelector(@selector(object))];
如果将object改为newObject则可以解决这个问题,以此验证上面的KVC查找规则。
五.KVC性能和私有访问
根据上面KVC的实现原理,我们可以看出KVC的性能并不如直接访问属性快,虽然这个性能消耗是微乎其微的。所以在使用KVC的时候,建议最好不要手动设置属性的setter、getter,这样会导致搜索步骤变长。
而且尽量不要用KVC进行集合操作,例如NSArray、NSSet之类的,集合操作的性能消耗更大,而且还会创建不必要的对象。
根据上面的实现原理我们知道,KVC本质上是操作方法列表以及在内存中查找实例变量。我们可以利用这个特性访问类的私有变量,例如下面在.m中定义的私有成员变量和属性,都可以通过KVC的方式访问。
这个操作对readonly的属性,@protected的成员变量,都可以正常访问。如果不想让外界访问类的成员变量,则可以将accessInstanceVariablesDirectly属性赋值为NO。
TestObject.m文件
@interface TestObject () {
NSObject *_objectOne;
}
@property (nonatomic, strong) NSObject *objectTwo;
@end
KVC在实践中也有很多用处,例如UITabbar或UIPageControl这样的控件,系统已经为我们封装好了,但是对于一些样式的改变并没有提供足够的API,这种情况就需要我们用KVC进行操作了。
可以自定义一个UITabbar对象,然后在内部创建自己想要的视图,并通过layoutSubviews方法在内部进行重新布局。然后通过KVC的方式,将UITabbarController的tabbar属性替换为自定义的类即可。
补充:安全性检查
KVC存在一个问题在于,因为传入的key或keyPath是一个字符串,这样很容易写错或者属性自身修改后字符串忘记修改,这样会导致Crash。
可以利用iOS的反射机制来规避这个问题,通过@selector()获取到方法的SEL,然后通过NSStringFromSelector()将SEL反射为字符串。这样在@selector()中传入方法名的过程中,编译器会有合法性检查,如果方法不存在或未实现会报黄色警告。
[self valueForKey:NSStringFromSelector(@selector(object))];
// ViewController.m
// 14-KVO的使用
/*
KVO: Key Value Observing (键值监听)--->当某个对象的属性值发生改变的时候(用KVO监听)
*/
#import "ViewController.h"
#import "XMGPerson.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
XMGPerson *person = [[XMGPerson alloc] init];
person.name = @"zs";
/*
作用:给对象绑定一个监听器(观察者)
- Observer 观察者
- KeyPath 要监听的属性
- options 选项(方法方法中拿到属性值)
*/
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
person.name = @"ls";
person.name = @"ww";
}
- (void)dealloc {
// 移除监听
[person removeObserver:self forKeyPath:@"name"];
}
/**
* 当监听的属性值发生改变
*
* @param keyPath 要改变的属性
* @param object 要改变的属性所属的对象
* @param change 改变的内容
* @param context 上下文
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
// NSKeyValueChangeNewKey == @"new"
int new = [change[NSKeyValueChangeNewKey] intValue];
// NSKeyValueChangeOldKey == @"old"
int old = [change[NSKeyValueChangeOldKey] intValue];
NSLog(@"%@------%@------%@", keyPath, object, change);
}
@end
KVO在Apple中的API文档如下:
Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …
KVO基本原理:
1.KVO是基于runtime机制实现的
2.当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
3.如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
4.每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
5.键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。
KVO深入原理:
1.Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为: NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
2.NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;
3.所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。
4.(isa 指针的作用:每个对象都有isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。) 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。
5.子类setter方法剖析:KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法: 被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO原理图