KVO细节分析上
- 这里我们主要了解
context的细节如下图
image.png
关于context,苹果官方文档解释
### Context
The context pointer in the `addObserver:forKeyPath:options:context:` message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify `NULL` and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.
A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.
The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing. Listing 1 shows example contexts for the `balance` and `interestRate` properties chosen this way.
-
监听回调
image.png
-
移除观察者
image.png
现在编写代码如下
// 可以定义context标识符来区分监听的属性
static void *PersonNameContext = &PersonNameContext;
@interface LGViewController ()
@property (nonatomic, strong) LGPerson *person;
@end
@implementation LGViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.name = @"test";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
// 这里使用context来区分监听的属性
if(context == PersonNameContext){
NSLog(@"%@",change);
}
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name" context:PersonNameContext];
}
// 运行工程,点击页面打印如下
2021-09-04 12:07:04.887849+0800 001---KVO初探[10193:13690743] {
kind = 1;
new = test;
}
现在创建单例[LGStudent shareInstance];,页面销毁时候不移除属性监听,代码如下
@implementation LGDetailViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
// 单例
self.student = [LGStudent shareInstance];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.student.name = @"hello word";
}
#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"LGDetailViewController :%@",change);
}
- (void)dealloc{
}
运行单例工程,点击页面发生崩溃
image.png
进入一个VC里面有一个单例持有观察VC的属性,哪怕VC被释放 单例没释放继续观察,再次进入VC的时候观察属性发生变化,之前已经被释放的VC会继续接收到消息,因为已经被释放所以野指针崩溃
小结: 使用KVC,一定要注意添加观察者与移除观察者要一一对应
KVO细节分析下
- 手动观察属性
@implementation LGPerson
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
- (void)setName:(NSString *)name
{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
//监听name这个属性
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
//改变属性值
self.person.nick = [NSString stringWithFormat:@"%@+",self.person.nick];
- 监听一个
属性通过另外两个变量去控制
@implementation LGPerson
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (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];
}
//监听downloadProgress这个属性
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
//改变这两个属性
self.person.writtenData += 10;
self.person.totalData += 1;
- 对
可变数组的观察
image.png
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
self.person.dateArray = [NSMutableArray array];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"nb"];
2021-09-04 12:53:15.899991+0800 001---KVO初探[10500:13724184] {
kind = 1;
new = (
);
}
2021-09-04 12:53:15.900726+0800 001---KVO初探[10500:13724184] {
indexes = "<_NSCachedIndexSet: 0x6000022c0c00>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
nb
);
}
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
kind=2的类型是insert
KVO原理上
KVO的原理描述
-
KVO的自动键值观察是使用isa-swizzling技术实现的; -
isa指针,顾名思义指向维护调度表的对象的类。这个调度表本质上包含指向类实现的方法的指针,以及其他数据; - 当对象的属性注册为观察者时,将会修改被观察对象的
isa指针,指向一个中间类而不是真正的类。因此isa指针的值不一定反映实例的实际类; - 不应该依赖
isa指针来决定类的成员。相反应该使用类方法来确定对象实例的类
KVO原理分析
image.png
- 只对属性观察
setter,不能对成员变量进行监听。而属性和成员变量的区别在于属性比成员变量多一个setter方法,而KVO监听的就是setter方法 - 中间类 -
self.person->LGPerson isa发生了变化NSKVONotifying_LGPerson(LGPerson 子类) - 有什么东西 - 方法 - 属性
setNickName - class - dealloc - _isKVOA
继承-重写-实实在在的实现 -
setter子类 - 父类改变nickName传值
willchange父类的setterdidChange -
NSKVONotifying_LGPerson是否移除+isa是否会回来 在移除观察的时候
添加KVO监听的时候动态生成了NSKVONotifying_LGPerson
image.png
image.png
遍历类以及子类,查看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);
}
image.png
得出结论 LGPerson是NSKVONotifying_LGPerson的父类
KVO原理下
- 探索
NSKVONotifying_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);
}
// LGViewController.m文件
@implementation LGViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
}
// 运行工程打印如下
// 重写setNickName方法
2021-09-04 22:13:31.476760+0800 002---KVO原理探讨[11980:13924110] setNickName:-0x7fff207bab57
2021-09-04 22:13:31.477058+0800 002---KVO原理探讨[11980:13924110] class-0x7fff207b9662
2021-09-04 22:13:31.477224+0800 002---KVO原理探讨[11980:13924110] dealloc-0x7fff207b940b
2021-09-04 22:13:31.477352+0800 002---KVO原理探讨[11980:13924110] _isKVOA-0x7fff207b9403
可以看到自动生成的类NSKVONotifying_Person中,有四个方法,分别是setNickName,class,dealloc,_isKVOA:
-
setNickName观察对象的setter方法 -
class类型 -
dealloc是否释放(该dealloc执行时,将isa重新指向Person) -
_isKVOA判断是否是KVO生成的一个辨识码
NSKVONotifying_Person中的方法是重写了父类的方法
- 判断当前
isa是否指回来?
image.png
在注册成为观察者之前,实例对象person的isa指向LGPerson,在注册成为观察者之后,实例对象person的isa指向NSKVONotifying_Person;,移除观察者之后实例对象person的isa又指向LGPerson
-
NSKVONotifying_LGPerson用完是否销毁?
image.png
KVO移除之后,生成的NSKVONotifying_LGPerson并不会销毁。
- 我们测试了属性
nickName的修改可以被KVO监听到,那么成员变量是否也能监听到呢?
给LGPerson类添加名为name的成员变量
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
// 分别为name和nickName都添加KVO监听
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
image.png
KVO只能监听属性,不能对成员变量进行监听;而属性和成员变量的区别在于属性比成员变量多一个setter方法,而KVO监听的就是setter方法
-
KVO监听的是setter方法,中间类NSKVONotifying_Person也重写了setter方法,那么我们最终修改的setter方法究竟是NSKVONotifying_Person的还是LGPerson的呢?
image.png
可以看到在移除观察者时,isa已经指向了LGPerson,而且nickName的值也改变了,那么此时的setter方法是LGPerson的
下面我们通过观察变量值改变来验证一下
image.png
继续运行项目,触发监听
image.png
bt打印堆栈信息
image.png
所以最终调用的setter是LGPerson的setNickName方法
小结
1.isa->LGPerson->NSKVONotifying_LGPerson-消失
1.1 动态生成 NSKVONotifying_LGPerson
1.2 LGPerson VS NSKVONotifying_LGPerson 父子关系
1.3 NSKVONotifying_LGPerson有哪些方法
setNickName //重写set方法
class
dealloc
_isKVOA
1.4 isa指回来 //通过这个方法_isKVOA
1.5 NSKVONotifying_LGPerson并没有销毁
2.setter方KVO实例法(setter)/class
2.1 set方法的意义是能够区分成员变量和属性变量
2.2 修改LGPerson属性setNickName方法










网友评论