iOS底层原理之KVO分析(上)

941 阅读4分钟

什么是KVO?

KVO是Key-Value Observing的简写,称为键值观察,用来监听对象属性的变化

KVO使用探究

我们以IFPerson类为例研究KVO的使用。

自动触发KVO监听

  • 主要代码
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.person.name = @"kvo";
    NSLog(@"class = %@",object_getClass(self.person));
    //对person的name添加kvo监听
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    NSLog(@"class = %@",object_getClass(self.person));
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.name = [NSString stringWithFormat:@"%@ +",self.person.name];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@" === %@",change);
    }
}

-(void)dealloc {
    //移除监听
    [self.person removeObserver:self forKeyPath:@"name"];
}

运行代码点击屏幕效果如下

手动触发KVO监听

如果想要手动触发KVO监听,需要重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key函数,该函数默认返回YES,表示自动触发,返回NO,则表示手动触发。对于本文,我们判断下key == name则手动触发,否则自动触发。修改代码如下

//在`IFPerson.m`中
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
-(void)setName:(NSString *)name {
    [self willChangeValueForKey:name];
    _name = name;
    [self didChangeValueForKey:name];
}

从上述代码中,我们可以看到,要实现手动触发KVO,有两个步骤需要实现

  • 步骤一:重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key函数,根据需要返回NO
  • 步骤二:对于监听的对象属性发生改变时,需要在改变前后调用willChangeValueForKey:didChangeValueForKey:

KVO底层原理探究

从上述中我们可以看到我们可以根据KVO监听属性的变化,那么在底层是怎么实现的呢?

KVO监听前后对象的变化

我们在监听name前后获取personclass以及其指针地址看下是否有变化,代码如下

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.person.name = @"kvo";
    NSLog(@"class = %@ address = %p",object_getClass(self.person),self.person);
    //对person的name添加kvo监听
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    NSLog(@"class = %@ address = %p",object_getClass(self.person),self.person);
}

打印结果如下从上图中我们看到,self.personclass前后发生了改变,但是对象地址并没有改变。我们再通过获取isa来验证下。

  • 监听之前
  • 监听之后
  • 监听前后总结:从上面分析可知,在进行KVO监听后,对象的class即isa指向发生了改变,由IFPerson变成了NSKVONotifying_IFPerson,但是对象的地址没有改变

NSKVONotifying_IFPerson是什么?

我们知道了在进行KVO监听后class变成了NSKVONotifying_IFPerson,那么NSKVONotifying_IFPerson从何而来?下面我们看下NSKVONotifying_IFPerson的父类是谁。

准备工作

由于superClass是隐藏属性,在外部无法访问,因为我们定义一个结构体,并强转下类型来获得superClass,代码如下

struct cw_objc_class {
    Class _Nonnull isa;
    Class _Nullable super_class;
};

// 强转一下类型
    struct cw_objc_class *newClass = (__bridge struct cw_objc_class *)object_getClass(self.person);
    NSLog(@"superClass = %@",newClass->super_class);

从打印结果来看NSKVONotifying_IFPerson父类竟然是IFPerson

NSKVONotifying_IFPerson类实现了哪些方法?

既然runtime在进行KVO监听的时候派生了NSKVONotifying_IFPerson类,那么NSKVONotifying_IFPerson类跟IFPerson类实现方法有什么区别呢?我们打印下监听前后两个类的实现方法区别

#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}
NSLog(@"监听之前方法:");
    [self printClassAllMethod:object_getClass(self.person)];
    
    //对person的name添加kvo监听
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    NSLog(@"监听之前方法:");
    [self printClassAllMethod:object_getClass(self.person)];

打印结果如下从打印监听前后方法列表我们可以看到NSKVONotifying_IFPerson类有如下变化

  • 重写了被监听属性的setter方法,如setName:
  • 修改了isa指针指向的地址,由原来的IFPerson指向了NSKVONotifying_IFPerson
  • 实现了dealloc方法
  • 增加了一个_isKOA标识符

isa指针何时重新指向IFPerson

既然isa指针在被监听后指向了NSKVONotifying_IFPerson类,那么何时重新指向IFPerson呢?答案是在移除监听的时候。为了验证,我们在dealloc中打印下监听前后isa指针的变化。

KVO总结

  • KVO在监听属性后,runtime会根据对象的类型生成一个NSKVONotifying_XXX(XXX是原来的类)派生类,该派生类NSKVONotifying_XXX继承于原来的类
  • NSKVONotifying_XXX重写被监听属性的setter方法,但是赋值的时候通过重写的setter内部消息转发还是通过原来类的setter方法来实现
  • KVO在移除监听时将isa指针指向地址由NSKVONotifying_XXX派生类重新指向原来类