什么是KVO?
KVO(Key-value observing)是一种允许对象观察(也可以叫监听)其他对象的指定属性发生变化的机制。当被观察对象的指定属性发生变化时,它会向观察者对象发送通知,告知变化的属性及变化内容,以便观察者根据也无需求进行处理。KVO是建立在KVC的基础之上的,所以要了解KVO,必须先了解KVC(点击了解KVC的底层原理)。要了解更多内容可以参考官方文档。
KVO的使用
KVO的使用主要有三个部分组成,分别是注册观察者、接收观察对象变化通知和移除观察者。下面我们通过demo来演示KVO的使用及其过程。demo代码结构如下:
@interface LNUser : NSObject
@property (nonatomic, copy) NSString *nameAndAge;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) NSMutableArray *friends;
+ (LNUser *)userWithName:(NSString *)name age:(NSInteger)age;
@end
@interface ViewController ()
@property (nonatomic, copy) LNUser *user;
@property (nonatomic) ThreeFloats threeFloats;
@end
注册观察者(Registering as an Observer)
KVO对象通过注册一个观察者(Observer)来负责观察自己相关属性变化的通知,通过如下方法来注册观察者:
/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- observer
这里的observer就是观察者,负责观察对象的属性变化,观察者需要实现如下的回调方法来接收被观察对象变化的通知。
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context;
- options
这里的options是一个枚举,可以选择观察属性变化的新值还是旧值,也可以同时观察两个,其定义如下:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
- context
这里的context顾名思义就是KVO观察的上下文,用于标记每一次观察的细节, 观察者在收到通知时可以根据context唯一确定是不是我们要观察的对象和keyPath。它的作用比如说有多个对象、多个属性时可以通过context快速区分,提高性能;而且有时候通过keyPath来判断的话,有可能keyPath有重名的情况,比如父类和子类的属性名称一样,这样会对数据安全构成威胁,这时候就需要context唯一标记每一个观察。context通常是一个静态void *变量的指针,如demo中:
static void *kUserNameUpdateContext = &kUserNameUpdateContext;
static void *kUserNameAndAgeUpdateContext = &kUserNameAndAgeUpdateContext;
static void *kUserFriendsUpdateObserContext = &kUserFriendsUpdateObserContext;
通过KVO可以观察对象的某个属性,也可以观察某个属性的关联属性,也可以观察集合属性等。接下来我们展示几种KVO常用方式。
1、观察对象属性
下面demo中self观察self.user的name属性,只要name属性有变化,self就会接到通知:
[self.user addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:kUserNameUpdateContext];
2、观察多个相关的属性
这里我们假设有一个属性nameAndAge,它的值跟name和age都有关系,也就是只要name或者age的值变化nameAndAge也跟着变化,我们可以这样做:先注册self观察nameAndAge,同时LNUser实现keyPathsForValuesAffectingValueForKey方法来设置name和age之间的关联,demo如下:
[self.user addObserver:self forKeyPath:@"nameAndAge" options:NSKeyValueObservingOptionNew context:kUserNameAndAgeUpdateContext];
在LNUser.m中实现keyPathsForValuesAffectingValueForKey方法,如下:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"nameAndAge"]) {
NSArray *affectingKeys = @[@"name", @"age"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
当我们给对象注册观察者时,被观察对象会调用keyPathsForValuesAffectingValueForKey方法,我们重写这个方法来达到我们自定义处理的目的。
3、观察数组属性
观察数组属性具体不只是属性的set方法,还能观察数组的insert、remove等操作。前面我们说过KVO是建立在KVC的基础之上的(KVC的原理),实际上这里观察的数组就是KVC中设计到的insert、remove相关的方法,接下来我们以demo来演示:
[self.user addObserver:self forKeyPath:@"friends" options:NSKeyValueObservingOptionNew context:kUserFriendsUpdateObserContext];
self.user.friends = [[NSMutableArray alloc] init];
LNUser *user = [LNUser userWithName:@"WW" age:18];
LNUser *user2 = [LNUser userWithName:@"YY" age:19];
[[self.user mutableArrayValueForKey:@"friends"] addObject:user];
[[self.user mutableArrayValueForKey:@"friends"] insertObject:user2 atIndex:0];
[[self.user mutableArrayValueForKey:@"friends"] removeObject:user];
[[self.user mutableArrayValueForKey:@"friends"] removeObjectAtIndex:0];
demo中如果想观察到friends的insert和remove操作,必须通过KVC的方式来访问数组(mutableArrayValueForKey:方法)。这里还有一个要注意的地方,friends使用前要初始化,如果friends为空的话mutableArrayValueForKey: 会返回空导致崩溃。
接收观察对象变化通知(Receiving Notification of a Change)
当被观察对象对应的keyPath的value发生变化时,观察者会收到如下观察方法,观察方法会把变化的keyPath,变化的内容change和观察的上下文context回传回来,我们可以根据特定的context和keyPath自行处理。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == kUserNameUpdateContext) {
if ([keyPath isEqual:@"name"]) {
}
}else if(context == kUserNameAndAgeUpdateContext){
if ([keyPath isEqual:@"nameAndAge"]) {
}
}else if(context == kUserFriendsUpdateObserContext){
if ([keyPath isEqual:@"friends"]) {
}
}
NSLog(@"change:%@", change);
}
移除观察者
在观察者销毁前移除观察者,不然会出现crash的问题。通常我们会在观察者dealloc中移除观察者:
- (void)dealloc
{
[self.user removeObserver:self forKeyPath:@"name" context:kUserNameUpdateContext];
[self.user removeObserver:self forKeyPath:@"nameAndAge" context:kUserNameAndAgeUpdateContext];
[self.user removeObserver:self forKeyPath:@"friends" context:kUserFriendsUpdateObserContext];
}
完整demo:
static void *kUserNameUpdateContext = &kUserNameUpdateContext;
static void *kUserNameAndAgeUpdateContext = &kUserNameAndAgeUpdateContext;
static void *kUserFriendsUpdateObserContext = &kUserFriendsUpdateObserContext;
@interface LNUser : NSObject
@property (nonatomic, copy) NSString *nameAndAge;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) NSMutableArray *friends;
+ (LNUser *)userWithName:(NSString *)name age:(NSInteger)age;
@end
@implementation LNUser
- (instancetype)init
{
self = [super init];
if (self) {
self.friends = [[NSMutableArray alloc] init];
}
return self;
}
+ (LNUser *)userWithName:(NSString *)name age:(NSInteger)age
{
LNUser *user = [[LNUser alloc] init];
user.name = name;
user.age = age;
return user;
}
- (NSString *)nameAndAge
{
return [NSString stringWithFormat:@"%@:%@",_name,@(_age)];
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"nameAndAge"]) {
NSArray *affectingKeys = @[@"name", @"age"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (void)setName:(NSString *)name
{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqual:@"name"]) {
return NO;
}
return YES;
}
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"LNUser:{name:%@, age:%@}", _name, @(_age)];
}
@end
@interface ViewController ()
@property (nonatomic, copy) LNUser *user;
@property (nonatomic) ThreeFloats threeFloats;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.user = [LNUser userWithName:@"ZZ" age:19];
[self.user addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:kUserNameUpdateContext];
self.user.name = @"TTT";
[self.user addObserver:self forKeyPath:@"nameAndAge" options:NSKeyValueObservingOptionNew context:kUserNameAndAgeUpdateContext];
self.user.name = [self.user.name stringByAppendingString:@"8"];
self.user.age += 1;
[self.user addObserver:self forKeyPath:@"friends" options:NSKeyValueObservingOptionNew context:kUserFriendsUpdateObserContext];
LNUser *user = [LNUser userWithName:@"WW" age:18];
LNUser *user2 = [LNUser userWithName:@"YY" age:19];
[[self.user mutableArrayValueForKey:@"friends"] addObject:user];
[[self.user mutableArrayValueForKey:@"friends"] insertObject:user2 atIndex:0];
[[self.user mutableArrayValueForKey:@"friends"] removeObject:user];
[[self.user mutableArrayValueForKey:@"friends"] removeObjectAtIndex:0];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == kUserNameUpdateContext) {
if ([keyPath isEqual:@"name"]) {
}
}else if(context == kUserNameAndAgeUpdateContext){
if ([keyPath isEqual:@"nameAndAge"]) {
}
}else if(context == kUserFriendsUpdateObserContext){
if ([keyPath isEqual:@"friends"]) {
}
}
NSLog(@"change:%@", change);
}
- (void)dealloc
{
[self.user removeObserver:self forKeyPath:@"name" context:kUserNameUpdateContext];
[self.user removeObserver:self forKeyPath:@"nameAndAge" context:kUserNameAndAgeUpdateContext];
[self.user removeObserver:self forKeyPath:@"friends" context:kUserFriendsUpdateObserContext];
}
自动观察和手动观察
KVO观察分为自动观察和手动观察。被观察对象通过automaticallyNotifiesObserversForKey方法判断是否是自动观察。默认情况下返回YES,即是自动观察,比如我们前面的代码。那什么情况下是手动观察呢?为了实现手动观察我们得让被观察对象重写automaticallyNotifiesObserversForKey:,这里可以指定某个Key为手动观察,其他为自动:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqual:@"name"]) {
return NO;
}
return YES;
}
但是光实现这个方法还不够,我们还要手动调用willChangeValueForKey和didChangeValueForKey方法以通知观察者属性的变化。比如这里我们重写setName:方法:
- (void)setName:(NSString *)name
{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
备注:这里willChangeValueForKey和didChangeValueForKey必须成对出现才有效。
KVO的底层原理
KVO自动观察是通过isa-swizzling技术实现的。isa-swizzling大概意思就是isa指针交换。isa指针是指向类的指针,它的底层结构比较复杂(点击了解更多关于isa指针的信息)。当一个对象注册一个观察者观察它的属性时,它的isa指针就会被修改,这时的指针已经不是指向它原来的类,而是一个中间类,这个中间类是在KVO注册观察者时创建的,它是以对象原有的类为父类,新建一个子类,用于处理KVO。这样做的目的是为了不改变原来的类结构,因为要观察对象的属性变化,就必须要去修改类的结构,因为属性列表等信息是存储在类结构中的(点击了解更多类结构相关的信息)。但是如果是手动观察,就不会改变isa指针。接下来我们通过上面的代码运行调试,以验证结论。
验证isa指针变化及中间类的存在
以下通过读取注册观察前后self.user的类的变化来判断isa指针变化。
NSLog(@"注册手动观察前:%@", object_getClass(self.user));
[self.user addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:kUserNameUpdateContext];
NSLog(@"注册手动观察后:%@", object_getClass(self.user));
Class startClass = object_getClass(self.user);
NSLog(@"注册自动观察前:%@", startClass);
[self.user addObserver:self forKeyPath:@"nameAndAge" options:NSKeyValueObservingOptionNew context:kUserNameAndAgeUpdateContext];
Class endCls = object_getClass(self.user);
NSLog(@"注册自动观察后:%@", endCls);
打印结果:
2021-08-17 09:58:37.713304+0800 KVC&KVODemo[63178:16132888] 注册手动观察前:LNUser
2021-08-17 09:58:37.713608+0800 KVC&KVODemo[63178:16132888] 注册手动观察后:LNUser
2021-08-17 09:58:37.713752+0800 KVC&KVODemo[63178:16132888] 注册自动观察前:LNUser
2021-08-17 09:58:37.714362+0800 KVC&KVODemo[63178:16132888] 注册自动观察后:NSKVONotifying_LNUser
这其中name属性是手动通知,所以self.user对象LNUser在注册观察前后它的类没有变化,但是nameAndAge属性是自动观察的,所以注册观察者之后类发生了改变,由原来的LNUser变成了NSKVONotifying_LNUser,也就是isa指针发生了改变。
备注:着这里要特别注意的是,获取当前对象的类不能通过class方法获取,比如[self.user class],而是要通过object_getClass或者object_getClassName函数获取,因为这两个函数都是直接访问的isa指针,而class方法有可能会被重写(后面分析KVO对象的结构会有解释)。以下是object_getClass方法和object_getClassName方法实现:
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
const char *object_getClassName(id obj)
{
return class_getName(obj ? obj->getIsa() : nil);
}
所以后面demo我们都是用object_getClass函数来获取对应的类class的。
这个isa指针是动态变化的,当对象的观察者移除完毕之后就会,isa又改回为指向原来的类,以下是demo演示:
- (void)dealloc
{
NSLog(@"移除观察者前:%@", object_getClass(self.user));
[self.user removeObserver:self forKeyPath:@"name" context:kUserNameUpdateContext];
[self.user removeObserver:self forKeyPath:@"nameAndAge" context:kUserNameAndAgeUpdateContext];
NSLog(@"移除部分观察者后:%@", object_getClass(self.user));
[self.user removeObserver:self forKeyPath:@"friends" context:kUserFriendsUpdateObserContext];
NSLog(@"移除所有观察者后:%@", object_getClass(self.user));
}
打印结果:
2021-08-17 10:35:39.776155+0800 KVC&KVODemo[63872:16170818] 移除观察者前:NSKVONotifying_LNUser
2021-08-17 10:35:42.325530+0800 KVC&KVODemo[63872:16170818] 移除部分观察者后:NSKVONotifying_LNUser
2021-08-17 10:35:45.085174+0800 KVC&KVODemo[63872:16170818] 移除所有观察者后:LNUser
中间类会缓存
备注:这个中间类NSKVONotifying_LNUser创建之后会被缓存,即使我们把所有观察都移除了,被观察对象的isa已改回为指向初始的类,但是中间类会被缓存下来,下次程序再次注册观察者的时候会判断有没有中间类缓存,有缓存就是用,没有才会新建一个中间类。验证过程如下:
demo中第一次注册的观察者已经移除,对象的类由LNUser变成了NSKVONotifying_LNUser,cls1表示第一次注册时生成的NSKVONotifying_LNUser,然后第二次注册时cls3也是一个NSKVONotifying_LNUser类,我们通过x/4gx读取cls1和cls3的内存,发现他们是完全一致的,cls3复用了cls1,证明了缓存的存在。
验证中间类是LNUser的子类
观察注册自动观察前后的self.user的class和superClass,看看对应的类和父类:
Class startClass = object_getClass(self.user);
NSLog(@"注册自动观察前:%@", startClass);
NSLog(@"注册自动观察前superClass:%@", [startClass superclass]);
[self.user addObserver:self forKeyPath:@"nameAndAge" options:NSKeyValueObservingOptionNew context:kUserNameAndAgeUpdateContext];
Class endCls = object_getClass(self.user);
NSLog(@"注册自动观察后:%@", endCls);
NSLog(@"注册自动观察后superClass:%@", [endCls superclass]);
打印结果:
2021-08-17 10:55:36.264796+0800 KVC&KVODemo[64316:16193280] 注册自动观察前:LNUser
2021-08-17 10:55:38.071268+0800 KVC&KVODemo[64316:16193280] 注册自动观察前superClass:NSObject
2021-08-17 10:55:38.071953+0800 KVC&KVODemo[64316:16193280] 注册自动观察后:NSKVONotifying_LNUser
2021-08-17 10:55:38.072126+0800 KVC&KVODemo[64316:16193280] 注册自动观察后superClass:LNUser
注册观察者前,对象的isa指向LNUser,父类是NSObject;注册观察者后,对象的isa变成指向NSKVONotifying_LNUser类,父类是LNUser。证明了中间类是LNUser的子类。
验证中间类和LNUser结构上的区别
经过比较,NSKVONotifying_LNUser相对父类出现的变化主要在方法列表上,属性和成员变量并没有变化。下面专门针对方法列表作对比分析:
NSLog(@"开始打印LNUser的方法列表:");
[self printClassAllMethod:startClass];
[self.user addObserver:self forKeyPath:@"nameAndAge" options:NSKeyValueObservingOptionNew context:kUserNameAndAgeUpdateContext];
Class endCls = object_getClass(self.user);
NSLog(@"开始打印NSKVONotifying_LNUser的方法列表:");
[self printClassAllMethod:endCls];
打印结果:
2021-08-17 11:19:12.992325+0800 KVC&KVODemo[64972:16228215] 开始打印LNUser的方法列表:
2021-08-17 11:19:12.992521+0800 KVC&KVODemo[64972:16228215] nameAndAge-0x10f13b220
2021-08-17 11:19:12.992651+0800 KVC&KVODemo[64972:16228215] setNameAndAge:-0x10f13b5b0
2021-08-17 11:19:12.992761+0800 KVC&KVODemo[64972:16228215] init-0x10f13b0a0
2021-08-17 11:19:12.992856+0800 KVC&KVODemo[64972:16228215] copyWithZone:-0x10f13b590
2021-08-17 11:19:12.992953+0800 KVC&KVODemo[64972:16228215] name-0x10f13b5f0
2021-08-17 11:19:12.993161+0800 KVC&KVODemo[64972:16228215] .cxx_destruct-0x10f13b6c0
2021-08-17 11:19:12.993527+0800 KVC&KVODemo[64972:16228215] setName:-0x10f13b2d0
2021-08-17 11:19:12.993831+0800 KVC&KVODemo[64972:16228215] setAge:-0x10f13b640
2021-08-17 11:19:12.994116+0800 KVC&KVODemo[64972:16228215] age-0x10f13b620
2021-08-17 11:19:12.994381+0800 KVC&KVODemo[64972:16228215] friends-0x10f13b660
2021-08-17 11:19:12.994664+0800 KVC&KVODemo[64972:16228215] setFriends:-0x10f13b680
2021-08-17 11:19:12.994936+0800
KVC&KVODemo[64972:16228215] 开始打印NSKVONotifying_LNUser的方法列表:
2021-08-17 11:20:35.099860+0800 KVC&KVODemo[64972:16228215] setAge:-0x7fff207bfc81
2021-08-17 11:20:35.099980+0800 KVC&KVODemo[64972:16228215] setNameAndAge:-0x7fff207bf03f
2021-08-17 11:20:35.100102+0800 KVC&KVODemo[64972:16228215] class-0x7fff207bdb49
2021-08-17 11:20:35.100204+0800 KVC&KVODemo[64972:16228215] dealloc-0x7fff207bd8f7
2021-08-17 11:20:35.100305+0800 KVC&KVODemo[64972:16228215] _isKVOA-0x7fff207bd8ef
2021-08-17 11:20:35.100407+0800
根据打印结果,NSKVONotifying_LNUser相对于LNUser类多了一个_isKVOA方法,用于判断是否是KVO对象;另外还重写了几个方法setAge:、setNameAndAge:、class和dealloc。这其中setAge:和setNameAndAge是分别对应被观察的属性name和nameAndAge的set方法,重写这两个方法是为了在发生set操作时能给观察者发送通知;而重写class方法则是为了返回初始的类LNUser,避免返回中间类型NSKVONotifying_LNUser,这那样做的目的是为了不让开发者感知到当前对象的类的变化,因为这是一个中间类,开发者并不知道具体类名,如果开发者之前使用了class方法来获取类来判断是不是LNUser等操作,就可能会有问题,因此才会重写, 所以我们前面在获取观察类的变化时没有用class方法,而是用runtime的object_getClass函数;而重写dealloc则是为了在被观察对象销毁的时候作自己相应的处理。
观察者如何接受到通知
有上面的比较结果可知,属性变化通知主要应该是在set方法发起的。我们之前在做手动观察demo时其实也是在重写setName方法,然后手动调用相关方法进行通知观察者:
- (void)setName:(NSString *)name
{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
但是自动观察是不是这样的呢?由于苹果不开放KVO源码,我们没知道知道具体实现,但是我们可以利用断点调试查看大致流程。前面我们知道nameAndAge是被自定观察的属性,因此我们在nameAndAge的set方法打个断点,看看KVO是如何给观察者发送通知的:
- (void)setNameAndAge:(NSString *)nameAndAge
{
_nameAndAge = nameAndAge;
}
在这个方法中打个断点,然后进入汇编调试模式:
通过断点可知,在调用set方法前会先调用[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]方法,我们进入这个方法的汇编代码,滑到断点的位置,查看调用set方法前后KVO都做了那些操作。通过Xcode -> Debug -> Debug Workflow -> Always show disassambly 开启汇编调试模式:
定位到断点的位置,查看set前后的代码流程:
虽然没有看到手动观察时显式调用的方法willChangeValueForKey和didChangeValueForKey,但是我们看到类似的操作NSKeyValueWillChange和NSKeyValueDidChangeBySetting,原理上讲应该都是一样的。在set前后调用相关方法以达到通知观察者相关Key的value变化的目的。
上面的调试有一点需要注意的是,setNameAndAge方法调用进入的是LNUser的setNameAndAge方法,而不是我们之前我们分析的中间类NSKVONotifying_LNUser里面的,这是为什么呢?我们前面不是说了NSKVONotifying_LNUser会重写相应的被被观察属性的set方法吗?
重载父类的set方法
根据流程推测,应该是KVO子类重写的set方法里重载了父类的set方法。上面的setNameAndAge:最终是通过父类LNUser的实现来赋值的,这里面NSKVONotifying_LNUser虽然重写了set方法,但是他还是会重载父类的setNameAndAge:,只是在调用父类方法前后分别加了willChange和didChange相关的操作。这其实也很好理解,毕竟父类的set方法实现也比较完整,包括内存管理操作都已经实现了,没必要全部重写,重载父类方法只是进行必要的复用而已。
总结
本文主要参考苹果官方文档的介绍,并根据自己的理解进行分析。如果需要了解KVO更加详细的内容,可以参考官方文档。同时网上也有很多自定义KVO Demo,关于自定义KVO日后有空再说。