美文网首页iOS 进阶之路
OC底层原理二十三:KVO原理

OC底层原理二十三:KVO原理

作者: markhetao | 来源:发表于2020-10-29 16:47 被阅读0次

    OC底层原理 学习大纲

    上一节,我们介绍了KVC原理,而KVC工作,绝大部分通过继承NSObject自动处理好了。实际应用中,我们关注相对较少。而基于KVCKVO,在应用中却是非常的广泛。

    现在我们使用的响应式框架(RACRxSwifCombine等),实际都是KVO机制的应用

    本节,我们详细讲解KVO:

    1. KVO介绍
    2. KVO应用
    3. KVO原理

    引入:

    • 我们上一节分析KVC时,官方KVC的应用中,第一个介绍的就是KVO
      👉 KVC文档链接
      image.png

    我们点击进入Key-Value Observing Programming Guide (KVO指引)


    1. KVO介绍

    KVO,全称为Key-value observing键值观察。

    • 键值观察是一种机制,允许对象其他对象指定属性发生更改得到通知

    2 KVO应用:

    • 测试代码:(监听person对象的name属性的新值
    // HTPerson
    @interface HTPerson : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
    
    @implementation HTPerson
    @end
    
    // ViewController
    @interface ViewController ()
    @property (nonatomic, strong) HTPerson *person;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.person = [HTPerson new];
        self.person.name = @"ht";
        
        // 1. 添加
        [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
    }
    
    // 2. 监听
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        if ([keyPath isEqualToString: @"name"]) {
            NSLog(@"新值:%@", change[NSKeyValueChangeNewKey]);
        }
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
       self.person.name = [NSString stringWithFormat:@"%@ +",self.person.name];
    }
    
    -(void)dealloc {
        // 3. 移除
        [self.person removeObserver:self forKeyPath:@"name" context: NULL];
    }
    
    @end
    
    • 这里为了测试,在touchesBegan点击事件中添加了name的变更,多次点击,打印结果如下:
      image.png

    主要步骤: 1. 添加 -> 2. 监听 ->3. 移除

    2.1 添加

    • addObserver 添加操作中,addObserver是监听对象KeyPath是监听路径option是监听类型,是一个枚举,包含:
    typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
        NSKeyValueObservingOptionNew     // 新值
        NSKeyValueObservingOptionOld     // 旧值
        NSKeyValueObservingOptionInitial // 初始值 
        NSKeyValueObservingOptionPrior   // 变化前
    };
    

    面试官:添加通知时,context写什么内容?
    答:填nil
    面试官:回去等通知 😂

    • 关于context的介绍:
    image.png
    • 照顾英语不好的同学,我们放上谷歌翻译

      image.png
    • context的类型为(void *),所以不能写nil。但可以写成Null
      如果写Null,会默认通过KeyPath路径去确定需要监听的对象。但是这种方法可能导致父类由于不同原因观察相同的路径,而产生问题。而且查询父类消耗的计算资源更多

    所以苹果建议我们可以static void*创建静态的context,这样的好处是:

      1. 仅从本类中查找当前context节省计算资源,更安全
      1. observeValueForKeyPath监听对象时,我们可以不再通过name去区分当前响应对象。而是使用context精准区分当前响应对象:
        image.png
      1. removeObserver移除对象时,可以通过context精准移除观察对象:
        image.png

    2.2 监听

    • observeValueForKeyPath 监听当前控制器的所有变化,change有以下4种情况:
    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
        NSKeyValueChangeSetting = 1,
        NSKeyValueChangeInsertion = 2,
        NSKeyValueChangeRemoval = 3,
        NSKeyValueChangeReplacement = 4,
    };
    
    • keyPathobjectcontext和上述一样。

    2.3 移除

    一定要移除! 一定要移除! 一定要移除!

    网上有很多说Xcode升级后,不再需要手动移除监听者。仅仅在当前页面操作时,确实不用处理。

    • 但如果业务变得复杂,对于同一对象属性,如果当前页面进行了添加、监听和移除,而其他页面只进行添加和监听,再触发监听时,就会产生KVO Crash。所以我们要养成谁使用谁销毁的习惯。

    2.4 开关

    • automaticallyNotifiesObserversForKey控制自动手动发送通知默认值为自动 👉 官方介绍
    image.png
    • 当我们给HTPerson添加automaticallyNotifiesObserversForKey方法,返回值为NO后,所有监听消息都不再发送。
    • 我们重写属性setter方法,手动调用API进行消息发送:
    @implementation HTPerson
    +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        return NO;
    }
    
    -(void)setName:(NSString *)name {
        
        if ([self.name isEqualToString: name]) return; // 值没变化,不操作
        
        [self willChangeValueForKey:@"name"]; // 即将改变
        _name = name; // 赋值
        [self didChangeValueForKey:@"name"]; // 已改变
    }
    @end
    

    2.5 路径处理

    • 我们已downloadProgress下载进度为例,下载进度等于writtenData已下载数据量 / totalData总数据量

    • 我们可以聚合writtenDatatotalData两个属性,变成监听downloadProgress一个属性。

    // HTPerson
    @interface HTPerson : NSObject
    @property (nonatomic, copy) NSString *downloadProgress;
    @property (nonatomic, assign) double writtenData;
    @property (nonatomic, assign) double totalData;
    @end
    
    @implementation HTPerson
    
    - (NSString *)downloadProgress {
        if (self.writtenData == 0) {
            self.writtenData = 10;
        }
        if (self.totalData == 0) {
            self.totalData = 100;
        }
        return [NSString stringWithFormat:@"%.2f", 1.0f * self.writtenData / self.totalData];
    }
    
    // 下载进入 writtenData / totalData
    +(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
        NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        
        if ([key isEqualToString:@"downloadProgress"]) {
            NSArray * affectingKeys = @[@"totalData", @"writtenData"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
        }
        return keyPaths;
    }
    @end
    
    // ViewController
    @interface ViewController ()
    @property (nonatomic, strong) HTPerson *person;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person = [HTPerson new];
    
        // 添加监听
        [self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context: NULL];
        
    }
    
    // 处理监听
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        if ([keyPath isEqualToString: @"downloadProgress"]) {
            NSLog(@"当前进度:%@", change[NSKeyValueChangeNewKey]);
        }
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.person.writtenData += 20;
        self.person.totalData +=10;
    }
    
    -(void)dealloc {
        // 移除监听
        [self.person removeObserver:self forKeyPath:@"downloadProgress" context: NULL];
    }
    
    @end
    
    • 打印结果:


      image.png
    • 这里将writtenDatatotalData都变化了,可以看到每次打印的是2条记录。说明聚合的downloadProgress中,只要writtenDatatotalData值变化,都会触发一次。
      (如果你用过RACRxSwift,此处一定非常熟悉,这就是RxSwift的merge的原理。)

    • 相关原理: 👉 官方链接

    2.6 数组的观察

    • 集合等类型的监听,与属性的监听不同。我们可以查阅KVC的官方文档
    image.png
    // HTPerson
    @interface HTPerson : NSObject
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, strong) NSMutableArray *dateArray;
    @end
    
    @implementation HTPerson
    
    @end
    
    // ViewController
    @interface ViewController ()
    @property (nonatomic, strong) HTPerson *person;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person = [HTPerson new];
    
        self.person.name = @"ht ";
        self.person.dateArray = [NSMutableArray new];
        
        // 添加监听
        [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
        [self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context: NULL];
    }
    
    // 处理监听
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"%@: %@", keyPath, change);
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.person.name = [NSString stringWithFormat:@"%@+", self.person.name];
    //    [self.person.dateArray addObject:@"6"]; //此赋值仅改变数组内部元素,不会引起数组地址的变化
        [[self.person mutableArrayValueForKeyPath:@"dateArray"] insertObject:@"6" atIndex:0];
        [[self.person mutableArrayValueForKeyPath:@"dateArray"] setObject:@"8" atIndexedSubscript:0];
        [[self.person mutableArrayValueForKeyPath:@"dateArray"] removeObjectAtIndex:0];
    }
    
    -(void)dealloc {
        // 移除监听
        [self.person removeObserver:self forKeyPath:@"name" context: NULL];
        [self.person removeObserver:self forKeyPath:@"dateArray" context: NULL];
    }
    
    @end
    
    • 打印结果:
    image.png
    • 数组设值,必须使用专属API才可以触发。直接赋值仅改变数组内部元素不会引起数组地址变化

    • 从打印的结果上,change4种情况:

    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
        NSKeyValueChangeSetting = 1,
        NSKeyValueChangeInsertion = 2,
        NSKeyValueChangeRemoval = 3,
        NSKeyValueChangeReplacement = 4,
    };
    
    image.png

    3. KVO底层原理

    3.1 KVO只观察Setter方法

    • 我们先观察一个案例,此案例有public声明的nickName成员变量和@property定义的属性name,分别监听这2个属性值:
    • 测试代码:
    // HTPerson
    @interface HTPerson : NSObject
    {
    @public NSString * nickName;
    }
    @property (nonatomic, copy) NSString *name;
    @end
    
    @implementation HTPerson
    
    @end
    
    // ViewController
    @interface ViewController ()
    @property (nonatomic, strong) HTPerson *person;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person = [HTPerson new];
    
        self.person.name = @"ht ";
        
        // 添加监听
        [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
        [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
    }
    
    // 处理监听
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"%@: %@", keyPath, change);
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.person.name = [NSString stringWithFormat:@"%@+", self.person.name];
        self.person->nickName = [NSString stringWithFormat:@"%@+", self.person->nickName];
    }
    
    -(void)dealloc {
        // 移除监听
        [self.person removeObserver:self forKeyPath:@"name" context: NULL];
        [self.person removeObserver:self forKeyPath:@"nickName" context: NULL];
    }
    
    @end
    
    • 打印结果:


      image.png
    • 发现只监听属性的变化,而监听不到成员变量的变化。而属性成员变量区别,核心在于是否实现setter方法。

    3.2 KVO派生类

    • addObserver处打上断点运行到断点处后,打印当前person类:

      image.png
    • 惊奇的发现,使用运行时object_getClassName读取的self.person类,是NSKVONotifying_HTPerson类。而使用self.person.class直接打印的类,确是HTPerson类

    • 好像发现了一些不可告人的密码 😃 苹果金屋藏娇生成了个NSKVONotifying_HTPerson,但又故意的不让外部知道,所以调用class方法,打印的还是HTPerson类

    • 当然,从开发的层面,可以理解,对外事务越简单越好,高内聚减轻开发人员学习使用成本。不过对于我们现在探究底层原理而言,就想知道这个NSKVONotifying_HTPerson是什么。

    3.2.1 NSKVONotifying_HTPersonHTPerson什么关系?

    • 导入#import <objc/runtime.h>,添加打印本类所有子类的方法:
    /// 遍历本类及子类
    -(void) printClasses: (Class)cls {
        
        // 注册类的总数
        int count = objc_getClassList(NULL, 0);
        // 创建1个数组
        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);
    }
    
    • addObserver前后前后打印self.person类
    image.png
    • 观察到添加观察者后,HTPerson多了一个NSKVONotifying_HTPerson子类。

    我们添加遍历IvarsPropertyMethod的函数:

    /// 遍历Ivars
    -(void) printIvars: (Class)cls {
        
        // 仿写Ivar结构
        typedef struct HT_ivar_t {
            int32_t *offset;
            const char *name;
            const char *type;
            uint32_t alignment_raw;
            uint32_t size;
        }HT_ivar_t;
    
        // 记录函数个数
        unsigned int count = 0;
        // 读取函数列表
        Ivar * ivars = class_copyIvarList(cls, &count);
        for (int i = 0; i < count; i++) {
            HT_ivar_t * ivar = (HT_ivar_t *) ivars[i];
            NSLog(@"ivar: %@", [NSString stringWithUTF8String: ivar->name]);
        }
        free(ivars);
        
    }
    
    /// 遍历属性
    -(void) printProperties: (Class)cls {
        
        // 仿写objc_property_t结构
        typedef struct Ht_property_t{
            const char *name;
            const char *attributes;
        }Ht_property_t;
    
        // 记录函数个数
        unsigned int count = 0;
        // 读取函数列表
        objc_property_t * props = class_copyPropertyList(cls, &count);
        for (int i = 0; i < count; i++) {
            Ht_property_t * prop = (Ht_property_t *)props[i];
            NSLog(@"property: %@", [NSString stringWithUTF8String:prop->name]);
        }
        free(props);
        
    }
    
    /// 遍历方法
    -(void) printMethodes: (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(@"method: %@-%p", NSStringFromSelector(sel), imp);
        }
        free(methodList);
    }
    
    • addObserver处添加打印代码,分别检查HTperson本类和NSKVONotifying_HTPerson派生类的IvarsPropertyMethod
        // 添加监听
        [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
        [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
        NSLog(@"------- NSKVONotifying_HTPerson --------");
        [self printMethodes: objc_getClass("NSKVONotifying_HTPerson")];
        [self printIvars: objc_getClass("NSKVONotifying_HTPerson")];
        [self printProperties: objc_getClass("NSKVONotifying_HTPerson")];
        NSLog(@"------- HTPerson --------");
        [self printMethodes: HTPerson.class];
        [self printIvars: HTPerson.class];
        [self printProperties: HTPerson.class];
    
    • 打印结果:
    image.png

    拓展:

    • 检验同样继承HTPerosn的子类HTStudent,打印结果:
    // HTStudent
    @interface HTStudent : HTPerson
    @end
    @implementation HTStudent
    @end
    
    image.png

    结论:

    • 直接继承的子类,没有任何方法属性。可以确定:
      KVO派生类继承自HTPerosn,重写了setNameclassdealloc方法,新增了_isKVOA方法

    3.2.2 KVO派生类给父类属性赋值

    • addObserver处添加断点,运行代码到此处时,lldb输入:watchpoint set variable self->_person->_name
    image.png
    • 设置成功后,运行代码点击屏幕触发touchesBegan事件,会进入汇编页面(观察到设置属性断点处)
    image.png image.png
    • 可以观察到,当派生类在调用willChangedidChange中间,调用了[HTPerson setName]方法,完成了给父类HTPersonname属性赋值。(此时的willChange和didChange方法是继承自NSObject的)

    3.2.3 KVO派生类何时移除,是否真移除?

    • 我们在removeObserver处加入断点,分别在removeObserver前后使用object_getClassName()打印当前isa指向的

      image.png
    • 发现NSKVONotifying_HTPerson在外部removeObserver时,完成的移除操作。将isa指回了原类

    • 但是我们在removeObserver移除操作之后,打印HTPerosn类和子类的信息,发现NSKVONotifying_HTPerson派生类并没有移除

    ps: 页面销毁之后再打印HTPerosn类和子类,也一样存在NSKVONotifying_HTPerson派生类。

    • KVO派生类只要生成,就会一直存在,这样可以减少频繁的添加操作

    至此,我们已经知道KVO是创建派生类实现了键值观察

      1. 添加:addObserver时,创建了派生类,派生类是当前类的子类重写被监听属性setter方法,并将当前类isa指向派生类
        此时开始,所有调用本类的方法,都是调用的派生类派生类没有的方法,就会沿着继承链查询到本类
      1. 赋值: 派生类重写了被监听属性的setter方法,在派生类setter方法触发时:在willChange之后,didChange之前,调用父类属性settter方法,完成父类属性的赋值`。
      1. 移除: 在removeObserver后,isa派生类指回本类。 但创建过的派生类,不会被本类从子类列表移除,会一直存在。
      1. 假象: 之所以外部打印class永远看不到派生类,是因为派生类class方法重写了,故意不让外界看到。
        (知道越多,烦恼越多 😂 ,就让派生类做个默默付出的无名英雄吧)

    下一节,我们纯代码自定义KVO。(简化版,重在理解派生类流程功能

    相关文章

      网友评论

        本文标题:OC底层原理二十三:KVO原理

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