美文网首页
第二十三节—KVO(二)原理探索

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

作者: L_Ares | 来源:发表于2020-11-12 22:02 被阅读0次

    本文为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 <Foundation/Foundation.h>
    
    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<UITouch *> *)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<NSKeyValueChangeKey,id> *)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<UITouch *> *)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(二)原理探索

          本文链接:https://www.haomeiwen.com/subject/wuepbktx.html