OC原理-KVO

882 阅读3分钟
- (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强应用,不用考虑循环引用的问题。