KVO

基本使用:
说明:HJPerson是继承自NSObject的自定义类

@interface ViewController ()
@property(nonatomic, strong) HJPerson *person1;
@property(nonatomic, strong) HJPerson *person2;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[HJPerson alloc] init];
    self.person1.age = 11;
    
    self.person2 = [[HJPerson alloc] init];
    self.person2.age = 22;
    
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"监听到了%@的%@的属性值改变了 -- %@ --- %@",object,keyPath,change,context);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    self.person1.age = 111;
    self.person2.age = 222;
}
@end

上面的代码对 person1 的 age 属性进行了监听,在对 person1 和 person2 做新值改变的时候都会走 HJPerson 的 Set 方法,但是为什么一个就能被监听,而另一个却不行。所以我们打断点看看 person1 和 person2 的 isa 指针到底做了什么?

KVO_第1张图片
image.png

通过打印我们看到两个对象的 isa 指针不太一样,也就是说:我们的对象一但添加了监听,那他的 isa 指针就不是指向他原来的类对象,而是指向了另一个类对象。没添加监听的,他的类对象还是原来的。但是我们项目中并没有声明或者创建过这样的类对象,这个对象是哪里来的?

  • 这个是系统 Runtimer 动态创建的类,这个是HJPerson的子类,他们的继承关系就是下面的这张图
KVO_第2张图片
image.png

oc 中的方法调用是以消息为准的,那我们上面的这个代码 self.person1.age = 11; 这个实际就是找到 isa 指针的对象进行发消息,那系统动态生成的这个类大致做了一个什么事情,我们在自己的代码里面模仿声明一个 NSKVONotifying_HJPerson 这样的类,这个类中的用伪代码大致实现这个逻辑:

- (void)setAge:(NSInteger)age{
   _NSSetIntValueAndNotify();
}

void _NSSetIntValueAndNotify{
   [self willChangeValueForKey:@"age"];
   [super setAge:age];
   [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSInteger)age{
   ///< 通知监听器。某某属性值发生了改变
   [observer observeValueForKeyPath:age ofObject:self change:nil context:nil];
}

系统生成的这个类他自己会有SetAge的这个方法,它自己会调用Founction框架中的_NSSetIntValueAndNotify方法,他大致做了三件事,监听属性将要改变,之后对父类的成员变量进行赋值,之后通过监听者把消息发送出去。这个就是伪代码的实现,可以通过反编译的方式去越狱之后拿到 Founction 文件之后,用Hopper反编译工具,去查看_NSSetIntValueAndNotify 方法。

  • 怎样才能知道 _NSSetIntValueAndNotify 这个方法确定存在,我们通过打印 IMP 来看一下
   NSLog(@"person1 添加KVO之前的监听 %@ %@",object_getClass(self.person1),object_getClass(self.person2));
   NSLog(@"person1 添加KVO之前的监听(方法) %p %p",
         [self.person1 methodForSelector:@selector(setAge:)],
         [self.person2 methodForSelector:@selector(setAge:)]
         );
   NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
   [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
   NSLog(@"person1 添加KVO之后的监听 %@ %@",object_getClass(self.person1),object_getClass(self.person2));
   NSLog(@"person1 添加KVO之前的监听(方法) %p %p",
         [self.person1 methodForSelector:@selector(setAge:)],
         [self.person2 methodForSelector:@selector(setAge:)]
         );
KVO_第3张图片
image.png
  • 我们可以看到监听之后的方法发生了改变。也就是说添加监听之后他的方法实现就变成了_NSSetLongLongValueAndNotify 这个方法实现,也就是说运行时生成的这个类,确实是调用了Founction框架中的_NSSetLongLongValueAndNotify。
  • 现在我们来验证我们伪代码中的调用逻辑是否正确,在HJPerson类中。我们做一个这样的操作
- (void)setAge:(NSInteger)age{
   _age = age;
   NSLog(@" setAge ---> %zd",age);
}

- (void)willChangeValueForKey:(NSString *)key{
   [super willChangeValueForKey:key];
   NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key{
   NSLog(@"didChangeValueForKey----- begin");
   [super didChangeValueForKey:key];
   NSLog(@"didChangeValueForKey----- end");
}
KVO_第4张图片
image.png

我们可以看到这个里面打印顺序,
willChangeValueForKey
setAge
didChangeValueForKey----- begin
接收到监听的通知打印
didChangeValueForKey----- end

这样就验证了我们刚才在伪代码中实现的打印顺序

系统运行时生成的类做了什么:

 NSLog(@"类对象 -- >  %@----%@",object_getClass(self.person1),object_getClass(self.person2));
 NSLog(@"类对象 -- >  %@---%@",[self.person1 class],[self.person2 class]);
image.png
  • 我们发现用运行时获取类对象 和 通过 class方法 获取类对象得到的结果是不同的,为什么会产生这样的结果的?也就是说,通过runtime拿出来的isa 所指向的对象为真实对象,而通过 class 拿出来的对象就不一定了。也就是说,rutime 在生成这个类的时候重写了class方法。所以用 class 调用的还是 HJPerson 的这个类(屏蔽了内部实现,不让我们知道NSKVONotifying_HJPerson 这个类的实现)

  • 现在我们回到上面的继承关系的哪张图。我们怎么知道系统生成的类,有那些方法

- (void)printMethordNameOfClass:(Class)cls{
   unsigned int outCount;
   ///< 获取方法列表
   Method *methodList = class_copyMethodList(cls, &outCount);
   NSMutableString * methordNames = [NSMutableString string];
   ///< 遍历所有的方法
   for (int i = 0; i< outCount; i++) {
       Method methord = methodList[I];
       NSString *methordName = NSStringFromSelector(method_getName(methord));
       [methordNames appendString:methordName];
       [methordNames appendString:@" , "];
   }
   NSLog(@"methordNames --- > %@",methordNames);
   free(methodList);
}

- (void)viewDidLoad {
   [super viewDidLoad];
   [self printMethordNameOfClass:object_getClass(self.person1)];
   [self printMethordNameOfClass:object_getClass(self.person2)];
}
image.png

通过运行时打印出方法列表,我们就知道他内部的方法了。

所以通过上面这些。我们就知道KVO做了那些操作

  • 利用RuntimeAPI 动态生成了一个子类,并且让Instance对象指向了这个动态生成的子类。
  • 当修改instance对象的属性时,回调用Founction的_NSSetLongLongValueAndNotify函数
  1. willChangeValueForKey
    2.父类原有的setter方法
    3.didChangeValueForKey
    4.内部触发监听器(oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)

手动触发KVO

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
   [self.person1 willChangeValueForKey:@"age"];
   [self.person1 didChangeValueForKey:@"age"];
}

那些属性可以监听

有setter方法的都可以监听。因为系统运行时创建的类,就是通过替换了原有的
setter方法的实现。

直接修改成员变量会触发KVO嘛

@interface HJPerson : NSObject{
   @public
   NSInteger _age;
}
self.person1->_age = 2;

这样是不会触发KVO的,因为KVO 的本质是修改Setter方法。这样修改成员变量是不可以的

你可能感兴趣的:(KVO)