iOS-KVC中我们讲述了KVC的官方文档,从KVC的文档中引出KVO文档
因为有了KVO,才让OC有了响应式编程方式
定义KVO
定义3个ViewController
:
- 第一个页面只负责跳转;
- 第二个页面进行对象的监听;
- 第三个页面中也不做处理
其中第二个ViewController
中的代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
self.student = [LGStudent shareInstance];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.name = [NSString stringWithFormat:@"%@+", self.person.name];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name" context:NULL];
}
这样,我们就做了一个对LGPerson
的name
做了个监听。查看打印内容
到这里,相信没有人不懂的,那么下面的方法的参数具体是什么呢
// observer: 观察谁,弱引用添加
// keyPath:观察什么
// options:观察什么的变化,枚举值 - 新值、旧值
// context:上下文 为了进行观察这区分,解决问题,可以在监听回调方法中减少判断,代码可读性会降低
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
根据自己的理解再查看官方文档。
image
发现我们的猜测是相同的
remove的作用
我们都知道,在做完监听时,需要将监听移除,不移除会出现什么问题呢?
(有人说在iOS 10之后系统做了优化,无需移除监听,通过实测可知,系统并没有做优化,但是当监听的是当前对象的属性的某个属性时,不会出现崩溃问题,而监听的是某个单例的属性时就会出现)
查看官方文档可知
image
我们也可以从中知晓,当没有添加观察者,但却进行了移除观察者时,会导致崩溃,崩溃信息为:
image
查看文档后,为了保证安全,我们可以将remove代码写在try中,如下:
@try {
[self.person removeObserver:self forKeyPath:@"name" context:NULL];
} @catch (NSException *exception) {
NSLog(@"exception = %@", exception);
} @finally {
}
这样可以保证不会崩溃。
手动添加观察以及自动添加观察
使用automaticallyNotifiesObserversForKey:
方法可以实现对当前对象的某个属性的自动观察做开关处理。
我们在LGPerson.m
文件中做如下处理:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
运行代码后,查看可知,现在已经无法进行观察了。说明已经取消了自动观察。
那么在取消自动观察的时候,应该怎么做才能让他继续观察呢?
我们将上方对name
的观察修改为对nick
的观察,并在LGPerson
中重写setNick
的方法,代码如下:
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
运行可知,又可以对nick
进行观察了,但对name
的观察仍然是取消状态。
路径处理
比如我们监听下载的进度时,可知下载的进度 = 已下载量 / 总下载量,可我们又不想监听两个量,这个时候可以使用keyPathsForValuesAffectingValueForKey
方法进行观察处理。代码如下:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
// 数组中的两个值为真正需要监听的两个属性名。
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
// 重写 downloadProgress的getter方法用于取进度值
- (NSString *)downloadProgress{
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
将手动监听打开,运行后结果:
image
数组观察
数组的观察可能用到的不多,但是确实是可用,我们先做如下操作
// 在 viewDidLoad中 添加 dataArray 数组的监听
self.person.dateArray = [NSMutableArray array];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
// 在点击事件内 对数组进行修改
[self.person.dateArray addObject:@"1"];
// 在 dealloc中 对数组监听进行移除
[self.person removeObserver:self forKeyPath:@"dateArray"];
观察上述代码是否有问题,思考一会儿。
我们来运行代码,查看结果,可知并没有进入监听的回调方法中,但打印dateArray数组发现该数组确实变化了。
image
上节课在KVC中我们可知,数组中的键值与普通属性不同,那么KVO应该也不同,查看官方文档。
image
根据官方文档做如下处理,将修改数组的位置修改为
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
运行代码可知可以进行监听数组了
image
从打印信息可知,数组的监听与普通的属性的打印信息不同,即打印中的kind的值。
查看kind的值,如下:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1, // 设置
NSKeyValueChangeInsertion = 2, // 插入
NSKeyValueChangeRemoval = 3, // 移除
NSKeyValueChangeReplacement = 4, // 替换
};
上方的2
对应数组的addObject
等操作,3
对应数组的removeObject
等操作,4
对应数组的replaceObjectAtIndex:withObject:
的操作
KVO底层原理探索
观察的是什么?
重新定义LGPerson
类,自定义成员变量name
与属性nickName
,源码如下:
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
在ViewController
中做name
与nickName
的监听等操作,查看打印。
通过打印结果可知,KVO只对属性进行监听,对成员变量不监听. 属性与成员变量的区别在于属性存在 setter方法,而成员变量没有setter
中间类
self.person
的isa
的指向发生了变化
通过断点打印可知,在添加监听之后,self.person
的isa
重新指向了NSKVONotifying_LGPerson
类,对比之前的类多了NSKVONotifying_
前缀。
派生类:即某个类的子类
自定义方法,查看LGPerson的子类情况,代码如下:
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[I]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
并在self.person
添加监听的前后进行调用该方法,查看打印结果。
其中LGStudent
是LGPerson
的一个子类,在添加监听方法后发现LGPerson
多了一个子类NSKVONotifying_LGPerson
,说明添加监听方法后,self.person
的isa
指向了LGPerson
的子类。
中间类中有什么
是否存在什么方法。
自定义代码打印类的方法。
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[I];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
调用该方法
[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
查看打印结果。
image
从打印结果可知,在NSKVONotifying_LGPerson
类中添加了四个方法,分别为:setNickName
、class
、dealloc
、_isKVOA
这四个方法。
-
_isKVOA
判断当前是否为KVO类 -
dealloc
释放 -
setNickName
需要查看是继承还是重写的,答案重写的方法 class
判断setNickName是继承的还是重写的
通过打印另一个继承类LGStudent类进行。
image
发现LGStudent
类中什么都没有,那么我们在LGStudent
中重写一下setNickName
方法,再次打印查看结果。
这个时候发现LGStudent
中存在setNickName
方法,由此可知NSKVONotifying_LGPerson
中的setNickName
为重写的方法
isa是否会还原
NSKVONotifying_LGPerson
类移除后,self.person
的isa
是否会指回来。
在ViewController
中的dealloc
方法中添加断点,查看移除之后的self.person
的isa
的指向。
通过打印可知,在移除监听后,self.person
的isa
会重新指向LGPerson
。
在pop
的ViewController
也做LGPerson
的子类的打印,结果如下:
发现当监听被移除后,NSKVONotifying_LGPerson
类并没有被移除,而是仍然存在。
结论
- 对象的isa会重新指向
- 动态生成的类会一直存在
重写的setter方法内做了什么处理
猜测一下,首先,在调用NSKVONotifying_LGPerson
重写setter
方法的时候,改变的是其父类LGPerson
的nickName
的值,那么在重写的setter
方法中一定有对父类nickName
进行传值的操作。
设置观察self->_pserson->_nickName
,具体命令为:
watchpoint set variable self->_person->_nickName
image
执行命令后的结果。进入断点。
image
查看左侧堆栈,可知2、3、4步是隐藏逻辑为汇编语言
image
堆栈编号1为LGPerson
的setter
方法
堆栈编号2在断点前后分别执行NSKeyValueWillChange
方法以及NSKeyValueDidChange
方法。
在willChange
与didChange
之间执行了父类的赋值方法。
结论
在willChange
与didChange
之间调用父类的赋值方法,因此,父类的值得以改变。
优秀三方框架
FBKVOController
网友评论