iOS底层原理-KVO(下)

927 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

从上一篇iOS底层原理-KVO(上)根据苹果官方文档的定义和示例代码,初步了解了KVO在实际开发中如何去使用,但KVO是如何通过对象的属性进行监听的?当对象属性改变时又是如何通知外部对象的;KVO观察的是对象的setter方法,那实现KVO的过程setter方法做了什么呢?通过这篇文章我们来探索一下KVO的底层原理。

原理分析

代码调试

HomeVC.m

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"首页";
    
    self.person = [[ATPerson alloc] init];
    [self.person addObserver:self
                  forKeyPath:@"name"
                     options:NSKeyValueObservingOptionNew
                     context:NULL];
}

HomeVC中添加ATPersonname属性的监听,在添加监听之前打个断点(29行),输出当前的对象的类名。 01.png 可以看到输出ATPerson,然后在添加监听之后再输出对象的类名(33行处打个断点),运行。 02.png 可以看到输出了NSKVONotifying_ATPerson,在添加监听之后类由原来的ATPerson变成了NSKVONotifying_ATPerson,而NSKVONotifying_ATPerson是由系统动态生成的,这2个类到底有什么关系呢?我们通过打印ATPerson类及其所有子类看一下。

NSKVONotifying_ATPerson和ATPerson的关系

首先定义一个打印本类和所有子类的方法。

// 遍历类以及子类
- (void)printClasses:(Class)cls {
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i < count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

然后在添加KVO监听的前后分别打印,看前后的输出类的信息。 03.png 这里先做个说明,ATStudentATPerson的子类,通过打印发现,在添加KVO之前打印出ATPerson以及子类ATStudent,而添加KVO之后打印了ATPerson以及子类NSKVONotifying_ATPersonATStudent,而通过打印NSKVONotifying_ATPerson发现它并没有子类。通过上面的分析,得出一个结论:

  • NSKVONotifying_ATPersonATPerson的子类
  • NSKVONotifying_ATPerson没有子类 接下来我们研究一下NSKVONotifying_ATPerson类里包含了哪些方法?

NSKVONotifying_ATPerson的方法

还是通过定义一个方法来打印输出类的所有方法的函数。

// 遍历方法
- (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);
}

然后在viewDidLoadKVO监听后加上这个函数的调用,看看NSKVONotifying_ATPerson都包含了哪些方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"首页";
    
    self.person = [[ATPerson alloc] init];
    [self.person addObserver:self
                  forKeyPath:@"name"
                     options:NSKeyValueObservingOptionNew
                     context:NULL];
    [self printClassAllMethod:objc_getClass("NSKVONotifying_ATPerson")];
}

然后点击运行 04.png 从输出可以看到打印了4个方法

  • setName
  • class
  • dealloc
  • _isKVOA

从上面的输出可以看到NSKVONotifying_ATPerson重写了ATPerson的方法(setNameclassdealloc),_isKVOA是KVO的标识。我们在移除KVO监听时NSKVONotifying_ATPerson会有什么变化吗?接下来通过代码验证一下。

NSKVONotifying_ATPerson销毁

验证过程:HomeVCpush到SecondVC,在SecondVC做KVO监听,然后在dealloc里移除监听,在SecondVCdealloc打断点,来输出查看NSKVONotifying_ATPerson是否已被销毁。 05.png 当点击返回pop到上一视图时,这时走到SecondVCdealloc方法,在42行代码处此时还是输出NSKVONotifying_ATPerson,当跳到43行代码处已经移除了KVO监听,这时再打印类的信息发现输出ATPerson。因此我们得到了下面的结论:

  • 当添加KVO监听时,isa指针会指向动态生成的类NSKVONotifying_ATPerson
  • 当移除监听时,isa指针又指回了ATPerson。 通过上面的分析,isa指针发生了改变,那NSKVONotifying_ATPerson是否从注册的类里删除了呢?

NSKVONotifying_ATPerson类注册信息

可以在返回到HomeVC时,在HomeVCtouchBegin方法里打印ATPerson子类的情况。 06.png 返回到首页点击屏幕打印子类信息可以看到NSKVONotifying_ATPerson类并没有从类注册里删除,所以当KVO监听后注册了相关的NSKVONotifying_xxx的类,就会保留,下次添加监听时无需再次注册。

KVO的setter方法

根据上面的输出知道KVO只监听setter方法,也就是只监听属性变化,不监听成员变量的改变,下面通过代码来验证一下。

// 在ATPerson.h中定义一个成员变量hobby和一个属性name
@interface ATPerson : NSObject {
    @public
    NSString *hobby;
}

@property (nonatomic, copy) NSString *name;

@end

然后在SecondVC中添加属性name和成员变量hobby的监听,在touchesBegin方法中分别对它们赋值,运行看KVO监听回调的日志打印。 07.png

从输出日志可以看出KVO只对setter方法的监听,也就是属性的监听。 上面的setter方法是NSKVONotifying_ATPersonsetter方法,那我们在dealloc移除监听时打印一下self.person的属性值是否有变化。 08.png 当在touchesBegin点击给name赋值,在dealloc移除监听后,打印self.person.name依然是存在刚刚KVO监听的值的,因此从底层是通过KVO的setter方法修改了属性,实际也修改了外部ATPerson的属性。

断点调试

接下来通过断点调试来验证一下KVO的整个流程。还是在self.person添加name的监听后打个断点,然后对name属性进行观察。 09.pngname属性观察成功后,跳过当前断点,点击屏幕触发改变name属性值,然后输入bt,查看执行的堆栈信息 10.png 当点击屏幕触发属性值改变时,从堆栈信息可以看到以下的调用流程:

  1. KVODemo`-[SecondVC touchesBegan:withEvent:]
  2. Foundation`_NSSetObjectValueAndNotify + 269
  3. Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68
  4. Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 646
  5. KVODemo`-[ATPerson setName:]

从上面的调用流程可以看到,当KVO属性变化时,不是直接对外部类通过setter方法改变属性,而是有动态生成的中间类NSKVONotifying_xxx重写了相关的setter方法,设置完成后再通过block的方式通知外部,进而改变外部类的属性值。而这些流程都是通过动态去完成,对于外部使用者来说是无感知的。

总结

本篇通过代码示例去验证了关于KVO底层的实现过程,虽然有关KVCKVO苹果没有提供源代码,但结合苹果的官方文档,以及自己在实际案例中去验证苹果的实现流程,结合上面的分析做个简单的总结:

  • 当通过KVO的方式对属性进行监听,会自动生成NSKVONotifying_xxx
  • NSKVONotifying_xxxxxx的子类
  • NSKVONotifying_xxx重写了setter方法
  • NSKVONotifying_xxx在改变属性值后会通知外部
  • 触发observeValueForKeyPath方法调用