文章目录
前言
这篇博客由两个问题引出
- iOS用什么方式实现对一个对象的KVO (KVO的本质)
- 如何手动触发KVO
- 直接修改成员变量会触发KVO吗
KVO的使用
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
比如我们有一个类 其中有一个age
属性
我们想要监听age属性的改变 从0到1的那种
self.person = [[Person alloc] init];
self.person.age = 10;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.age = 20;
}
摸它一下它就会变了
呜呜好想知道你什么时候变的喔
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"1"];
给它加上一个监视它的小东C 这个小东C在这个属性变化的时候就会告诉你
内鬼找到了家人们
之后怎么给你发消息呢 需要你实现一个方法 (给你当内鬼不得给点儿好处)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
//当监听对象的属性值发生改变时 调用方法
NSLog(@"%@ of %@ change %@", keyPath, object, change);
}
- keyPath : 监听的对象是谁
- object : 监听的对象属于谁 (不要以为你是痴汉监听了人家 人家就属于你哦 不一定的喔)
- change : 从前面的定义可以看出 是一个字典 存放了new值和old值
- context : 相当于是一个标记 跟前面添加监听的时候对应起来
当然了 记得dealloc
谁都不想让内鬼被发现的喔
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"age"];
}
KVO本质分析
比如我们有两个对象
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"person1"];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person1.age = 21;
self.person2.age = 22;
}
只给person1
添加了监听 为什么person1.age
可以走到- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
方法呢
聪明的小伙伴要说啦 因为给它加了监听呀 person2.age
没加呀
是对的 但是只是表象喔
分别打印person1
和person2
的isa
指针、
person1
的类变成了NSKVONotifying_Person
注意不能打印person1.age->isa
懂我意思吧
未使用KVO监听的对象
Person
类的实例对象里面存放了一个isa
指针
isa
指针指向了Person
类的类对象 类对象里面存放了isa
指针 superclass
指针
实例方法比如setter
getter
方法等
当然了 Person
类的isa
指针指向了Person的元类对象
在我们调用self.person2.age = 22
的时候 通过实例对象的isa
指针寻找Person
类中的setter
方法 然后调用
- (void)setAge:(int)age {
_age = age;
};
这是正常的对象 没有使用KVO监听的
使用了KVO监听的对象
Runtime
动态创建的一个类 叫NSKVONotifying_Person
它是Person
类的一个子类
也就是Person
类的实例对象的isa
指针指向了NSKVONotifying_Person
类对象
说的高级一点
使用了KVO监听的实例对象的isa
指针会指向一个动态创建的一个全新的类
这个类对象里面也会有isa指针
superclass
指针 实例方法等等 并且是以前定义的类的子类
NSKVONotifying_Person
类对象的superclass
指针指向了Person
类对象
而代码执行到self.person1.age = 22
的时候 实例对象的isa
指针寻找到了NSKVONotifying_Person
类中的setAge
方法
而这个setAge
方法跟Person
类中的不太一样哦
- (void)setAge:(int)age {
_NSSetIntValueAndNotify();
}
酱紫的
这就导致了没有监听和监听了的 调用的setter
方法是不一样的 调用别的方法也是一个原理
大概就是这样的 大概喔大概 网上都这么说的 伪代码 流程差不多就是这样的
- (void)setAge:(int)age {
_NSSetIntValueAndNotify(); //这里监听的是int类型
//_NSSetXXXValueAndNotify()
}
void NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
本质分析验证
加入两行代码 调用object_getClass
函数验证一下子
通过重写上述_NSSetIntValueAndNotify();
伪代码中的两个方法进行验证
查看调用顺序是否正确
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"will change Value For Key Begin");
[super willChangeValueForKey:key];
NSLog(@"will change Value For Key end");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"did change Value For Key Begin");
[super didChangeValueForKey:key];
NSLog(@"did change Value For Key End");
}
逻辑清晰 条理清楚嗷家人们
在didChangeValueForKey
调用了监听器的方法
验证差不多了
子类的内部方法
有哪些
我们写个函数 得到一下类内部的方法列表
- (void)printMethodNamesOfClass:(Class)cls {
unsigned int count;
//获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
for (int i = 0; i < count; i++) {
//获得方法
Method method = methodList[i];
//获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
free(methodList);
NSLog(@"%@ - %@", cls, methodNames);
}
然后在self.person1
加入监听了之后调用一下
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
这样我们就得到了类对象的内部方法列表
class
通过[self.person1 class]
也可以得到类对象 上面验证的代码中 在添加了监听之后
通过runtime函数object_getClass
得到的类对象是先前说过的动态创建的类对象NSKVONotifying_Person
而调用[self.person1 class]
得到的是什么呢
NSLog(@"%@ -- %@", [self.person1 class], [self.person2 class]);
跟我们想的不太一样? 或者说 跟我写的不太一样
怎么事儿呢
所以NSKVONotifying_Person
类中重写了class
方法
- (Class)class {
return [Person class];
}
在我们调用[self.person1 class]
的时候 通过self.person1
的isa
指针 找到了NSKVONotifying_Person
类对象 在类对象中找到了class
类方法 最终调用 返回了Person class
那为什么要重写class
方法呢
因为苹果需要屏蔽内部实现 隐藏了这个类的存在 不希望开发者多想
如果一定要知道底层的实现 就使用runtime
函数咯
小总结
就是因为一个加了监听一个没加监听
导致了他们两个的isa
指针不一样 导致了他们找到的类对象不一样
也就导致了最后方法的实现不一样
前言中的关于KVO两个问题的解答
iOS用什么方式实现对一个对象的KVO? (KVO的本质是什么)
- 利用RuntimeAPI动态生成一个子类 并且让instance对象的isa指针指向这个全新的子类
- 当修改instance对象的属性时 会调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey
- 父类原来的
setter
didChangeValueForKey
- 内部会触发监听器的监听方法
observeValueForKeyPath: ofObject: change: context:
如何手动触发KVO
- 手动调用
willChangeValueForKey
和didChangeValueForKey
这个有个注意点 因为我加在之前那个触发改变属性值的地方之后 思索了一下好像没什么屁用啊
后来我才想明白
这两句代码可以加在任何地方 都会直接触发- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
方法
可能是我自己没想明白 大家应该不会有这样的疑问
直接修改成员变量会触发KVO吗
肯定是不会的
直接修改之后如果加上手动触发的方法 就可以触发
另一个前言
关于KVC也有两个问题需要解答
- 通过KVC修改属性会触发KVO吗
- KVC的赋值和取值过程是怎样的 原理是什么
KVC的使用
首先是常用的KVC的API
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key ;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
前面两个是用来设置属性值的
后面两个是用来获取属性值的
通过KVC修改属性会触发KVO吗
会
KVC的赋值和取值过程是怎样的 原理是什么
赋值
通过传进来的key
寻找setKey:
方法 如果没有 就寻找_setKey:
方法
哪怕根本没有key
这个成员变量
[self.person1 setValue:@10 forKey:@"age"];
在类的定义和实现文件中
#import <Foundation/Foundation.h>
@interface Person : NSObject
//@property (nonatomic, assign) int age;
@end
#import "Person.h"
@implementation Person
- (void)setAge:(int)age {
NSLog(@"setAge = %d", age);
}
@end
可以看到我们已经注释了age
属性 但是重写了setAge
方法
验证一下
如果没有setKey
方法和_setKey
方法的话
会调用
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
返回值的意思是是否允许直接访问成员变量
如果是NO 那setValue:forKey
方法会直接报错
如果是YES 会根据_key
_isKey
key
isKey
的顺序来访问成员变量
找到就修改 没找到就报错咯
取值
[self.person1 valueForKey:@"age"];
- (int)getAge {
return 1;
}
- (int)age {
return 2;
}
- (int)isAge {
return 3;
}
- (int)_age {
return 4;
}
查找的时候会按照这四个方法的顺序查找
都没找到
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
查看这个方法的返回值 默认是YES
如果是NO的话 报错
如果是YES
根据_key
_isKey
key
isKey
的顺序来访问成员变量 获得成员变量的值