- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.age = 11;
self.person2.age = 22;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@ 被修改了属性%@ ----%@",object,keyPath,change);
}
运行上诉代码,点击屏幕,我们发现都执行了属性改变,但只有person1的属性改变被监听了,而person2的属性改变没有被监听到。他们的差距就是person1执行了下面的一行代码。
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
我们不禁会好奇这行代码对person干啥了啥?
在执行这行代码的前后我们分别打印person1的isa指针如下
说明执行完这行代码后,person1的类对象都发生了转变。NSKVONotifying_Person类是哪冒出来的呢???
NSKVONotifying_Person类是使用runtime动态创建的一个类,是Person类的子类。
方法实现也发生了改变,说明添加kvo后在执行setAget:方法会去调用Foundation框架中的_NSSetIntValueAndNotify函数。 _NSSetIntValueAndNotify中的Int是根据KVO监听属性的类型来的。自然还有_NSSetDoubleValueAndNotify等函数。
_NSSet*ValueAndNotify的内部实现
\\伪代码 这里的self执行者是person1
void _NSSet*ValueAndNotify(){
[self willChangeValueForKey:@""];
调用父类即Person类修改属性值 。。
[self didChangeValueForKey:@""];
}
//这个方法在Foundation框架中NSKeyValueObserVing.m中,这个方法中去通知监听者
-(void)didChangeValueForKey:(NSString *)key{
//通知监听者 某某属性发生了改变
}
证明如下我们在person类中添加如下代码 执行属性改变
@implementation Person
-(void)setAge:(int)age{
_age = age;
NSLog(@"setAget:");
}
-(void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
-(void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey--start");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey--end");
}
@end
我们注释掉[super didChangeValueForKey:key];
再去执行属性修改,就发现无法触发kvo了。
如何手动触发KVO 不修改属性值的情况下
//手动这两个代码必须都有
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
KVO派生类添加的方法
我们使用下面方法打印出来NSKVONotifying_Person类的方法列表
//typedef struct objc_method *Method;
//struct objc_method {
// SEL method_name;
// char *method_types;
// IMP method_imp;
//};
//Selector,Method,IMP 它们之间的关系可以这么解释:
//一个类(Class)持有一个分发表,在运行期分发消息,表中的每一个实体代表一个方法(Method),它的名字叫做选择子(SEL),对应着一种方法实现(IMP)。
- (void)printSelectorNameOfClass:(Class)cls{
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
NSString * methodName = NSStringFromSelector(method_getName(method));
IMP imp = method_getImplementation(method);
NSLog(@"%@",methodName);
NSLog(@"%p", imp);
}
NSLog(@"done");
free(methodList);
}
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
[self printSelectorNameOfClass:object_getClass(self.person1)];//一定要添加kvo后再执行,这时候self.person1的类对象才是NSKVONotifying_Person
其中setAge:的实现是_NSSetIntValueAndNotify
其他三个函数的大体作用
-(Class)class{
//返回Person类对象 方便使用。 屏蔽内部实现 隐藏了NSKVONotifying_Person的存在
}
-(void)dealloc{
//销毁时告诉监听者
}
-(void)_isKVOA{
retrn YES;
}
总结
KVO的原理:利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类,新生成的子类复写了class和dealloc方法,修改了setter的方法IMP。当修改instance的属性时候(调用set方法),调用Foundation的_NSSetValueAndNotify函数,************** _NSSetValueAndNotify函数内部1.执行willChangeVlaueForKey:函数2.执行父类原来的setter函数3.执行didChangeVlaueForKey:,didChangeVlaueForKey:内触发监听者的监听方法。
最后的最后 上一张图
补充
在被监控的对象中,有个开关,用来控制某个属性是否能被监听
//被KVO监控的属性才会执行这个方法
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqual:@"age"]) {
//该属性不能被监听
return NO;
}
return YES;
}
kvo不会对observe强应用,不用考虑循环引用的问题
[self.p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
- 给某个对象的属性添加2次KVO监听,属性发生改变时候,会出发2次监听方法。
- 在子线程修改属性,监听方法也会在子线程中执行。
- 给某个属性添加一次kvo监听,移除两次会奔溃。
Cannot remove an observer <ViewController 0x1381057a0> for the key path "age" from <Person 0x60000034d7c0> because it is not registered as an observer.'
- 添加两次相同的通知,发送一次通知,会执行两次监听方法,而且只需移除一次,多次移除也不会奔溃。
- 在子线程发送通知,监听方法也会在子线程中执行。
- 通知不会对observe强应用,不用考虑循环引用的问题。