KVO分析
KVO细节分析上
- 这里我们主要了解
context的细节
如下图
关于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;
- 对
可变数组
的观察
[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
父类的setter
didChange
-
NSKVONotifying_LGPerson
是否移除+isa
是否会回来 在移除观察的时候
添加KVO监听的时候动态生成了NSKVONotifying_LGPerson
遍历类以及子类,查看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
的呢?
可以看到在移除观察者时,isa已经指向了LGPerson
,而且nickName
的值也改变了,那么此时的setter方法是LGPerson的
下面我们通过观察变量值改变
来验证一下
继续运行项目,触发监听
image.pngbt打印堆栈信息
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
方法