0x01 用途
键值观察是一种机制:
- 对于观察的属性为NSObject类型:它允许将其他对象的属性的更改【属性内存地址的更改】通知给观察者
- 对于观察的属性为基本类型如整型,结构体什么的:当值改变就会通知给观察者
对于MVC
中model
层和controller
层之间的通信很有用。
-
API
要求使用KVO
-
为其他人设计
API
-
想获取私有变量并修改
简单使用
使用分为三步:
- 注册观察者
[obj addObserver:forKeyPath:options:context:]
- 设置观察者接收回调
- (void)observeValueForKeyPath:ofObject:change:context:
- 移除观察者
[obj removeObserver:forKeyPath:]
自定义使用
- 是否开启自动发送通知
// Person.m
// 方案一
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if(key isEqualToString:@"name"){
return NO; // name将不会被自动触发,如果需要发出通知需要手动触发
}
return YES;
}
// 方案二
// + (BOOL)automaticallyNotifiesObserversOfPropertyName 会根据属性名,生成下面方法
+ (BOOL)automaticallyNotifiesObserversOfName {
return NO; // 取消自动发送
}
- 如果需要手动发出通知
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
示例
-(void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
可变容器的监听
我们知道KVO
监听的事属性的内存地址的更改,对于非集合类型,我们调整其值就可以做到修改监听改变。
但是对于属性是集合来说,我们修改的是属性内部元素,属性的内存地址并未改变。
所以我们可以通过,重新生成一个集合,然后再赋值的方式触发监听。
// Person.h
@property (nonatomic, strong) NSMutableArray *banks;
// vc.m
NSMutableArray *tmpArr = self.person.banks;
[tmpArr addObject:[NSString stringWithFormat:@"中国银行:%d",arc4random()%4]];
self.person.banks = [tmpArr mutableCopy]; // 新赋值一个对象
如果每次都这样写一下然后再赋值也很麻烦,有没有便捷的方式呢?
有,KVC
中有方法,可以实现这个功能。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
// 功能相似,都是从对象中根据Key获取一个可变的代理对象(可读写)当操作完毕后就会生成一个新的可变对象覆盖原值。所以才会产生新的内存地址,进而触发KVO监听
0x02 原理推导
官方描述
通过查看苹果官方对KVO
实现介绍,可以发现是通过isa-swizzling
技术实现的。
当对一个对象的属性添加一个观察者时,被观察对象的isa
指针被修改,指向了一个中间类,而不是真正的类。
抛出几个问题作为跟踪理解:
- 生成的中间类是什么,何时生成?它与我们当前类是什么关系
- 我们看到手动出发时需要发送
willChangeValueForKey
,didChangeValueForKey
,那么是不是生成的中间类也做了这些事,在哪里做的。- 不移除监听会怎样?
中间类是什么
// Person
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *bank;
@end
// VC.m
{
self.person = [[Person alloc] init];
NSLog(@"\n指针:%p-当前类名:%s-父类:%@",self.person,object_getClassName(self.person),class_getSuperclass(object_getClass(self.person)));
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
NSLog(@"\n指针:%p-当前类名:%s-父类:%@",self.person,object_getClassName(self.person),class_getSuperclass(object_getClass(self.person)));
}
// 打印:
指针:0x600002f9e4c0-当前类名:Person-父类:NSObject
指针:0x600002f9e4c0-当前类名:NSKVONotifying_Person-父类:Person
通过上述打印可以发现:
- 当添加观察者时就会生成一个新的类,前缀为
NSKVONotifying_
的一个中间类。 -
NSKVONotifying_Person
是Person
的一个子类。
中间类做了什么
我们可以使用以下代码打印下中间类实现了哪些方法
unsigned int count;
Method *methods = class_copyMethodList(object_getClass(self.person), &count);
for (int i = 0; i< count; i++) {
Method m = methods[i];
printf("methodName:%s - %s\n",method_getName(m), method_getTypeEncoding(m));
}
// 输出
methodName:setName: - v24@0:8@16
methodName:class - #16@0:8
methodName:dealloc - v16@0:8
methodName:_isKVOA - B16@0:8
理解输出:v16@0:8
v16@0:8
返回值(v)偏移量(16) 参数1(@)偏移量(0) 参数2(:)偏移量(8)
返回值为void类型
第一个参数为OC对象类型
第二个参数类型为SEL类型
查看更多Type-Encoding
通过上述分析可以发现,中间类 NSKVONotifying_Person
一共生成了4个方法
-
setName:
: 重写父类Person
的方法 -
class
:用来返回类型,当我们直接打印self.person
返回的就是Person
,而不是NSKVONotifying_Person
。 -
dealloc
:销毁方法,移除监听的时候调用 -
_isKVOA
: 返回是否为一个KVO
类
接下来我们验证中间类:setName:
做了什么
我们在setName
处打上断点,精简下调用栈可以发现:
当我们调用self.person.name = @"222"
会先走KVO
的系列方法,然后才会去调用setName
。
这就说明在中间类中重写了setter
方法,优先处理了通知机制,然后调用父类的[super setName:]
* thread #1, -[Person setName:](self=0x0000600002e61000, _cmd="setName:", name=@"测试158") at Person.m:14:2
frame #1: -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 646
frame #2: -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68
frame #3: _NSSetObjectValueAndNotify + 269
frame #4: -[TmpViewController touchesEnded:withEvent:](self=0x00007ff599413740, _cmd="touchesEnded:withEvent:", touches=1 element, event=0x0000600001b35e00) at TmpViewController.m:71:14
kvo不移除监听会怎样
如果同一个对象有A
、B
两个监听者,当发现改变时,KVO
会通知这两个监听者。如果有监听者被释放,就会出现访问坏内存错误。
KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_SMASHED
怎么探究:暂且放放
使用注意点
- 回调函数只有一个,不允许自定义
注意代码逻辑的划分,避免所有逻辑处理都写在同一个函数中
-
NSString
类型keyPath
容易传错
对于自己定义可以在外部访问的变量使用
NSSTringFromSelector(@selector(xxx))
来实现。对于无法获取到的,只能使用key的方式,这个无法避免。
- 子类会拦截父类实现,需要使用
Context
进行区分
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (context == CURRENT_POINTER && object == xxx && [keyPath isEqualToString:@"contentSize"]) { // 判断类型
} else { // 回归父类调用,避免父类无法调用
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- 重复
remove
或者不remove
会导致crash
0x03 KVO
使用注意点
- 为什么有时监听
frame
无效,需要使用center
才行
我们有时会需要通过监听自定义view
的Frame
的变化去做某些事情,这是如果是我们动态调整了自定义view
的frame
会很明显的触发。
但是更多的时候我们想要监听的事系统对我们自定的view
做出了调整。
这个时候监听frame
就会是无效的,需要通过监听center
来实现。
为什么监听frame
是无效的?
Note: Although the classes of the UIKit framework generally do not support KVO,
you can still implement it in the custom objects of your application, including custom views.
# 尽管UIKit框架的类通常不支持KVO,但是您仍然可以在应用程序的自定义对象(包括自定义视图)中实现它。
我们可以发现通常KVO
是不支持UIKIt
框架中的类,它本身是为NSObject
类实现的功能,当然你也可以自定义一些操作来实现这些功能。
一个示例效果(KVO监听frame
无效,只能监听center
)
# 写了个物理仿真器,创建一些小视图从顶部掉下来,要求超出屏幕范围就移除掉
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
self.gravity = [[UIGravityBehavior alloc] initWithItems:tmpArr];
self.gravity.magnitude = 0.1;
[self.animator addBehavior:self.gravity];
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
MyTmpView *tmpV = [[MyTmpView alloc] initWithFrame:CGRectMake(arc4random()%(int)UIScreen.mainScreen.bounds.size.width, 0, 2, 2)];
tmpV.backgroundColor = UIColor.redColor;
[tmpV addObserver:self forKeyPath:@"center" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.view addSubview:tmpV];
[self.gravity addItem:tmpV];
}];
// 超出屏幕范围就移掉
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"center"]) {
UIView *tmp = (UIView *)object;
if (!CGRectContainsRect(self.view.frame, tmp.frame)) {
[self.gravity removeItem:tmp];
[tmp removeFromSuperview];
}
}
}
0x04总结
KVO
内部的实现细节还有很深,这次主要分析了KVO
的逻辑操作设计原理和使用注意点。
针对于KVO Crash
原因也只是理论上理解,尚未脚踏实步的探究下,后续会再完善。
参考:
苹果官网描述KVO
网友评论