第二十三节—KVO(二)原理探索

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

准备 : KVO官方文档——KVO实现细节。

看了一下官方文档关于KVO实现细节的描述,内容很少,但是也阐明了其实现的核心思想——isa-swizzling

这里先翻译一下 :

  1. KVO是通过isa-swizzling思想实现的。
  2. isa指向对象的类(不明白的看这里),这个类拥有着dispatch_tabledispatch_table存储指针,这个指针指着类中的方法的实现(imp),还指着其他的一些数据。
  3. 当一个对象的属性被添加了观察者之后,对象的isa指针被修改,指向一个中间类,而不是指向真正的类。因此,isa指针的值不一定反映的就是实例对象的实际类。(在KVO这里的一个小彩蛋,和之前说isa指向的时候有一点点小不一样)。
  4. 官方建议 : 永远不要依据isa指针来决定类的成员关系。应该依据class方法来确定实例对象的类。

OK,那么这里其实已经说出了KVO的核心实现思想——isa交换实现

一、明确 : KVO只观察实现setter的变量

  1. 随意创建一个Project,创建一个类,类拥有一个属性,一个成员变量。
  2. 实例化一个类的对象,并添加属性和成员变量的观察者都为ViewController
  3. 添加touchBegin方法,做到点击屏幕,就让属性和实例变量都发生变化。

JDPerson

/************************************JDPerson.h************************************/
#import 

NS_ASSUME_NONNULL_BEGIN

@interface JDPerson : NSObject
{
    @public
    NSString *jd_name;
}

@property (nonatomic, copy) NSString *jd_nickName;

@end

NS_ASSUME_NONNULL_END

/************************************JDPerson.m************************************/
#import "JDPerson.h"

@implementation JDPerson

- (void)setJd_nickName:(NSString *)jd_nickName
{
    _jd_nickName = jd_nickName;
}

@end

ViewController.m

#import "ViewController.h"
#import "JDPerson.h"

@interface ViewController ()

@property (nonatomic, strong) JDPerson *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self jd_kvo_init_class];
    [self jd_kvo_add_observer];
}

#pragma mark - 初始化
- (void)jd_kvo_init_class
{
    self.person = [[JDPerson alloc] init];
    self.person->jd_name = @"";
    self.person.jd_nickName = @"";
}

#pragma mark - 添加观察者
- (void)jd_kvo_add_observer
{

    //添加属性的观察者
    [self.person addObserver:self forKeyPath:@"jd_nickName" options:NSKeyValueObservingOptionNew context:NULL];
    //添加成员变量的观察者
    [self.person addObserver:self forKeyPath:@"jd_name" options:NSKeyValueObservingOptionNew context:NULL];

}

#pragma mark - 让被观察的属性发生变化
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.person->jd_name = @"changed_name_ljd";
    self.person.jd_nickName = @"changed_nick_name_LJD";
}

#pragma mark - 观察回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"观察回调 : %@",change);
}

#pragma mark - dealloc
- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"jd_name"];
    [self.person removeObserver:self forKeyPath:@"jd_nickName"];
}


@end

结果 :

图1.0.0.png

再操作 :

利用KVC给成员变量jd_name赋值 :

#pragma mark - 让被观察的属性发生变化
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.person->jd_name = @"changed_name_ljd";
    self.person.jd_nickName = @"changed_nick_name_LJD";
    [self.person setValue:@"kvc_changed_name_ljd" forKey:@"jd_name"];
}

结果 :

图1.0.1.png

结论 :

KVO观察的是拥有setter方法的变量,可以是成员变量,但是必须使用KVC进行赋值,属性则是可以直接赋值进行观察的。

二、isa-swizzling

还是上面的代码,在ViewController中的添加属性的观察者的代码行加上断点。

利用lldbpo看一下self.person的类是否有改变。

图2.0.0.png

self.person的类发生了变化,isa从指向着JDPerson,变为了指向NSKVONotifying_JDPerson

问题 :
那么NSKVONotifying_JDPerson这个类是一个什么性质的类?

思路 :
正常的来推测,它应该属于JDPerson的一个子类,为了验证,可以来获取JDPerson的子类列表,查看添加观察者前和添加观察者后,JDPerson的子类列表是否出现差异,是否增加了一个NSKVONotifying_JDPerson类 。

操作 :
ViewController中添加如下代码 :

#pragma mark - 获取类的子类列表
- (void)printClassSubClassList:(Class)cls
{
    //获取注册类的总数量
    int count = objc_getClassList(NULL, 0);
    //创建一个可变数组,包含着给定的类
    NSMutableArray *mutArr = [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])) {
            [mutArr addObject:classes[i]];
        }
    }
    
    free(classes);
    NSLog(@"classes = %@",mutArr);
    
}

并在添加观察者之前和之后分别调用,传入参数[JDPerson class],查看JDPerson的子类。

结果 :

图2.0.1.png

结论 :

KVO给对象的特定属性添加观察者之后,对象的isa指向了一个中间类,并且这个中间类是对象所属类的子类。

三、KVO的中间子类

上面也看到了,当给一个对象的属性添加了观察之后,会发现该对象的isa指向发生了改变,指向了一个继承于该对象父类的子类,并且以NSKVONotifying_作为前缀。

那么这里就要看一下这个NSKVONotifying_作为前缀的类都做了什么,才实现了KVO的观察能力。既然NSKVONotifying_xxx是一个类,它必然拥有着isasuperClasscache_tbits,这4个基本的要素。

superClass都在上面说过了。
isa,既然是类的isa那么必然指向的是元类,元类再指向根元类。
cache_t则不知道在KVO里面这个中间类要怎么用。
bits,因为KVO主要做的事情就是观察变化,观察setter,所以终点一定在bits这里,只有它拥有着属性、方法的钥匙。

先看一下中间类的方法是否对比原来的类的方法有发生一些变化。

3.1 中间子类的方法

操作 :
添加如下代码获取类的方法列表 :

#pragma mark - 获取类的方法列表
- (void)printClassMethodsList:(Class)cls
{
    NSLog(@"***************分割线上****************");
    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(@"***************分割线下****************");
}

该方法放在注册观察者前调用一次,在注册观察者后再调用一次。

结果如下 :

图3.1.0.png

NSKVONotifying_xxx这个中间类拥有着4个方法 :

  • setXxx : 重写被观察的属性的set方法。
  • class : 重写自己的class方法。
  • dealloc : 重写自己的dealloc方法。
  • _isKVOA : 判断是不是KVO生成的中间类。

结论 :

也就是说,在进行了KVO观察之后的对象,它的isa再指向的就是NSKVONotifying_xxx这个类,做的改变都会找到NSKVONotifying_xxxset方法来对属性进行更改。

3.2 中间子类的存在与销毁

现在只观察jd_nickName这个属性,不再观察jd_name这个成员变量了。

操作 :
jd_nickName发送通知之后,直接就移除观察者,来po一下self.person的类,看看添加过观察者的对象的isa是否一直指向中间类。

图3.2.0.png

结果 :
很明显,当观察者被移除之后,被观察的对象的isa重新指回了原来的类。

问题 :
中间类会被直接销毁吗?

操作 :
在移除观察者的代码下面,调用printClassSubClassList,打印查看JDPerson类的所有子类。

结果 :

图3.2.1.png

所以是没有销毁的,这种缓存的做法也减少了下次再添加观察者的时候的开支。

结论 :

NSKVONotifying_xxx这个中间子类会重写父类的setterdeallocclass方法。并且当观察者移除之后,中间类并不会被销毁,而是缓存起来,有需要的时候直接调用。

四、总结

  • KVO观察对象的特定属性发生变化的核心思想是利用isa-swizzling.
  • 被观察的特定属性所属的对象的isa会指向一个动态生成的中间类,并且中间类拥有一个统一的前缀NSKVONotifying_,中间类继承于对象的父类
  • 中间子类会重写3个方法 : setterclassdealloc,另外会自带一个判断是否是KVO生成的子类的方法 : _isKVOA
  • 当移除观察的时候,被观察的属性所属的对象的isa会重新指会原本的类。
  • 生成的中间子类不会被销毁,依然存在于原来类的缓存之中。

你可能感兴趣的:(第二十三节—KVO(二)原理探索)