iOS底层原理

小码哥底层原理笔记:KVO的本质

2020-05-07  本文已影响0人  chilim

使用KVO主要是监听属性的变化。简单的KVO如下:

@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person

@end
#import "Person.h"

@interface ViewController ()

@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
  
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
    // Do any additional setup after loading the view.
    
    //给Person对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person1.age = 20;
    self.person2.age = 20;
}
//当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change,context);
}

- (void)dealloc{
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

@end

打印:
监听到<Person: 0x2816ecc80>的age属性值改变了 - {
    kind = 1;
    new = 20;
    old = 11;
} - 123

KVO的本质

当我们给person1增加KVO后修改age的值会调用observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法。我们知道复制操作就是调用了set方法,我们可以重写Person类的set方法

- (void)setAge:(int)age{
    _age = age;
}

发现person1和person2都调用了这个setAge方法,说明person1对象肯定还执行了其他方法触发了observeValueForKeyPath方法。

我们知道setAge方法是放在Person类对象里面的,而instance对象的isa指针指向的就是Person类对象,我们分别打印看一下person1和person2的isa的值:

NSLog(@"类对象 - %@ %@", object_getClass(self.person1), object_getClass(self.person2));//相当于打印实例对象的isa
打印:
类对象 - NSKVONotifying_Person class Person class

我们发现person1被KVO后其isa指针指向了NSKVONotifying_Person类对象。这个类对象是OC Runtime运行时定义的一个新类,这个新类是Person的子类,其superClass指向Person类对象。

NSKVONotifying_Person类的结构

当我们对一个对象进行KVO后,系统会生成一个新类NSKVONotifying_XXX,并且改变该对象的isa指针指向这个新类的类对象

NSKVONotifying_Person的结构

我们可以打印一下这个新生成的类有哪些方法?

- (void)printMethodNamesOfClass:(Class)class{
    unsigned int count;
    //获取方法数组
    Method *methodList = class_copyMethodList(class, &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(@"%@ %@", class, methodNames);
}

//分别打印类里面的方法
    [self printMethodNamesOfClass:object_getClass(self.person1)];//NSKVONotifying_Person setAge:,class,dealloc,_isKVOA,
    [self printMethodNamesOfClass:object_getClass(self.person2)];//Person setAge:,age,

我们发现这个新生成的类重写了setAge,class,dealloc方法,新增了一个_isKVOA方法。我们大概猜测一下这几个方法的实现:
我们首先看下重写的setAdge方法:

[self.person1 setAge:21];//添加KVO后这个方法实现是(IMP) $1 = 0x00000001ab742aa0 (Foundation`_NSSetIntValueAndNotify)
    
    [self.person2 setAge:22];//没有添加KVO这个方法的实现就是Person(XMGTestProject`-[Person setAge:] at Person.m:13)

我们打印出上面两个调用的setAge方法发现添加KVO后调用setAge方法实际上是调用了Foundation的_NSSetIntValueAndNotify方法实现,而没有添加KVO的对象则调用Person类里面的方法。所以我们大概知道NSKVONotifying_Person类里面的setAge方法为

#import "NSKVONotifying_Person.h"

@implementation NSKVONotifying_Person
- (void)setAge:(int)age{
    _NSSetIntValueAndNotify();
}
@end

当我们调用setAge方法时,添加KVO后对象会通过isa指针找到NSKVONotifying_Person类方法里面的setAge方法。
_NSSetIntValueAndNotify这是一个随数据类型改变的一个函数,当age是double类型时,方法就变成了_NSSetDoubleValueAndNotify。
_NSSetIntValueAndNotify内部实现大概为

//伪代码
void _NSSetIntValueAndNotify(){//_NSSet**ValueAndNotify(){},不同数据类型不一样
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

NSKVONotifying_Person类重写class方法是为了隐藏NSKVONotifying_Person,不让外界知道,对此我们可以打印一下看看

NSLog(@"类对象 - %@ %@", [self.person1 class], [self.person2 class]);// Person Person

发现被KVO的对象self.person1打印出来还是Person,但是实际上已经变成NSKVONotifying_Person类了,我们可以通过runtime的object_getClass方法试下

NSLog(@"类对象 - %@ %@", object_getClass(self.person1), object_getClass(self.person2));//NSKVONotifying_Person Person

发现被KVO的对象self.person1打印出来是NSKVONotifying_Person,没有被KVO的还是Person。
而重写dealloc方法是为了释放一些对象。
至此,我们知道NSKVONotifying_Person大概就是长这样

#import "NSKVONotifying_Person.h"

@implementation NSKVONotifying_Person

- (void)setAge:(int)age{
    _NSSetIntValueAndNotify();
}
//伪代码
void _NSSetIntValueAndNotify(){//_NSSet**ValueAndNotify(){},不同数据类型不一样
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

- (Class)class{
// 得到类对象,在找到类对象父类
     return class_getSuperclass(object_getClass(self));
}

- (void)dealloc{
    
}

- (BOOL)_isKVO{
    return YES;
}

@end

最后我们来验证一下什么时候会调用这个方法
//当监听对象的属性值发生改变时,就会调用

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

我们知道KVO实际上就是重写了setAge方法,然后赋值的时候调用setter方法,从而触发上面这个回调的。现在我们大概知道了NSKVONotifying_Person类中的setter方法是

/伪代码
void _NSSetIntValueAndNotify(){//_NSSet**ValueAndNotify(){},不同数据类型不一样
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

那么就有可能是以上三行代码中的一行会触发这个回调。
我们在Person类中重写willChangeValueForKey和didChangeValueForKey方法

#import "Person.h"

@implementation Person

- (void)setAge:(int)age{
    _age = age;
}

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

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

@end
打印如下:
2020-05-07 10:26:43.179228+0800 XMGTestProject[84279:5634192] willChangeValueForKey -begin
2020-05-07 10:26:43.179477+0800 XMGTestProject[84279:5634192] willChangeValueForKey -end
2020-05-07 10:26:43.179625+0800 XMGTestProject[84279:5634192] didChangeValueForKey -begin
2020-05-07 10:26:43.179987+0800 XMGTestProject[84279:5634192] 监听到<Person: 0x2826d0a30>的age属性值改变了 - {
    kind = 1;
    new = 21;
    old = 1;
} - 123
2020-05-07 10:26:43.180179+0800 XMGTestProject[84279:5634192] didChangeValueForKey -end

很显然我们可以看到这个回调是在调用didChangeValueForKey方法时触发的。

总结:并不是所有属性都能使用KVO,有setter方法才能使用KVO,KVO就是重写setter方法

面试题

1、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
答:利用RuntimAPI动态生成一个子类,并且让instance对象的isa指针指向这个全新的子类。
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数,这个函数实现是:
willChangeValueForKey
父类原来的setter
didChangeValueForKey
内部会触发监听器(Observe)的监听方法observeValueForKeyPath: ofObject: change: context:
2、如何手动触发KVO?
答:我们一般是调用set方法赋值,从而自动触发KVO。
手动调用其实是调用willChangeValueForKey和didChangeValueForKey方法

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//    [self.person1 setAge:21];
//    [self.person2 setAge:22];
    //手动调用KVO
    [self.person1 willChangeValueForKey:@"age"];
    [self.person1 didChangeValueForKey:@"age"];
    
}

我们把set方法注释调换成手动调用模式,发现也能触发KVO收到回调。willChangeValueForKey和didChangeValueForKey必须同时调用,缺一不可。
3、直接修改成员变量是否会触发KVO?
答:不会,因为没有调用setter方法。因为KVO的本质是重写set方法,然后在set方法里依次调用willChangeValueForKey,原来的set方法,didChangeValueForKey,didChangeValueForKey内部会调用observer的observeValueForKeyPath: ofObject: change: context:方法

上一篇 下一篇

猜你喜欢

热点阅读