zoukankan      html  css  js  c++  java
  • kvo本质探寻

    一、概述

    1.本文章内容,须参照本人的另一篇博客文章“class和object_getClass方法区别”加以理解;

    2.基本使用:

    //给实例对象instance添加观察者,监听该实例对象的某个属性值的变化

    [self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"per1"];

    //监听值改变

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"监测到%@的属性%@已经改变%@---%@", object, keyPath, change, context);
    }

    二、代码分析

    1)添加kvo前后,类对象名称及其地址,以及setter方法地址对比:

    //代码

    - (void)test1
    {
        Class per1Class1 = object_getClass(self.per1);
        Class per2Class2 = object_getClass(self.per2);
        NSLog(@"添加kvo之前---className:%@ %@ %p %p", per1Class1, per2Class2, per1Class1, per2Class2);
        NSLog(@"添加kvo之前---setter:%p %p", [self.per1 methodForSelector:@selector(setAge:)], [self.per2 methodForSelector:@selector(setAge:)]);
        
        //基本使用
        [self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"per1"];
        
        Class per1Class3 = object_getClass(self.per1);
        Class per2Class4 = object_getClass(self.per2);
        NSLog(@"添加kvo之后---className:%@ %@ %p %p", per1Class3, per2Class4, per1Class3, per2Class4);
        NSLog(@"添加kvo之后---setter:%p %p", [self.per1 methodForSelector:@selector(setAge:)], [self.per2 methodForSelector:@selector(setAge:)]);
    }

    //打印

    2018-12-29 10:38:48.099671+0800 KVONatureSearch[1280:51086] 添加kvo之前---className:Person Person 0x105cbe058 0x105cbe058
    2018-12-29 10:38:48.100035+0800 KVONatureSearch[1280:51086] 添加kvo之前---setter:0x105cbb5f0 0x105cbb5f0
    2018-12-29 10:38:48.101003+0800 KVONatureSearch[1280:51086] 添加kvo之后---className:NSKVONotifying_Person Person 0x6000016ec900 0x105cbe058
    2018-12-29 10:38:48.101183+0800 KVONatureSearch[1280:51086] 添加kvo之后---setter:0x1065a6cf2 0x105cbb5f0

    分析:

    1.self.per1添加kvo之前,类名为Person, 类对象地址为0x105cbe058,setter方法为0x105cbb5f0,self.per1和self.per2是一样的;

    说明添加kvo之前,两个实例对象指向的是同一个类对象,而类对象在内存中仅有一份(仅创建一次);

    2.添加之后,self.per2对象依然指向之前的类对象,而self.per1指向的是一个新的类对象NSKVONotifying_Person,同时setter方法的地址也不一样

    2)添加kvo后,self.per1的isa指向的类对象的结构和self.per2的isa指向的类对象的机构的区别

    //代码

    - (void)test2
    {
        //基本使用
        [self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"per1"];
        
        [self fetchClassMethods:object_getClass(self.per1)];
        [self fetchClassMethods:object_getClass(self.per2)];
    }

    //打印

    2018-12-29 11:27:06.991414+0800 KVONatureSearch[1971:77513] NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,
    2018-12-29 11:27:06.991705+0800 KVONatureSearch[1971:77513] Person setAge:, age,

    分析:

    1.添加了kvo后的对象方法有四个:setAge:为属性的setter方法;class是一个对象方法(非类方法),这个方法有什么用,往后看;dealloc方法很好理解,销毁一些对象、内存管理等扫尾工作;_isKVOA字面理解为,即判断是否为kvo,返回一个BOOL值;

    2.为什么NSKVONotifying_Person类对象中没有getter方法age,而只有setter方法setAge:,往下看;

    //添加代码

        struct lyb_objc_class *per1Class = (__bridge struct lyb_objc_class *)object_getClass(self.per1);

        Class per2Class = object_getClass(self.per2);

        Class per1Class2 = [self.per1 class];

        //打断点

        int i = 0;

    //lldb模式:

    (lldb) po per1Class
    NSKVONotifying_Person
    
    (lldb) po per2Class
    Person
    
    (lldb) p/x per2Class
    (Class) $2 = 0x00000001096ca178 Person
    (lldb) p/x per1Class->super_class
    (Class) $3 = 0x00000001096ca178 Person

      (lldb) po per1Class2

      Person

    分析:

    1.很显然NSKVONotifying_Person类对象的super_class指针指向的地址和Person类对象的地址是一样的,即NSKVONotifying_Person类是Person类的子类;

    2.解决上述两个问题:

    <1>NSKVONotifying_Person类为什么不复写getter方法age,因为没有需求,kvo的功能是监听属性值的改变,即对setter方法的监听,不需要监听getter方法,直接用父类的就行;

    <2>class方法复写:问题是Person类并没有该方法,因为Person类继承自NSObject,点进去发现,class方法是NSObject的对象方法,如下图;

    而上述class方法返回的类对象per1Class2却是Person,这就苹果公司复写class方法的原因:目的就是规避子类NSKVONotifying_Person暴露,让开发者认为实例对象添加kvo后依然是原来的类对象,所以class方法复写大概如下:

    - (Class)class
    {
        return [Person class];
    }

    ——其实本质上NSKVONotifying_Person类也是Person类,那为什么要动态创建这样一个子类呢,往下看;

    3)实现原理:setter方法重写

    //添加代码

    - (void)test3
    {
        //基本使用
        [self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"per1"];
    }

    //打断点

     

    //打印

    2018-12-29 14:45:28.687540+0800 KVONatureSearch[6798:210888] touchesBegan
    2018-12-29 14:45:28.688275+0800 KVONatureSearch[6798:210888] 监测到<Person: 0x6000013aece0>的属性age已经改变{
        kind = 1;
        new = 20;
        old = 1;
    }---per1

    分析:

    1.很显然,只有per1被监听到了值的改变,而per2没有被监听;经断点调试,per1和per2两个对象值的改变,均调用了setAge:方法;所以Person类方法本身没有问题,跟kvo监听没有任何关系;

    2.如上述分析,per1的类对象为NSKVONotifying_Person,而per2的类对象为Person;而NSKVONotifying_Person为Person的子类,并对setAge:方法进行了重写;那么kvo监听方法(

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context)只能是在重写的setAge:方法中被调用,具体是如何实现的,往下看;

    //探寻setAge:方法复写

    //更改代码

    - (void)test3
    {
        NSLog(@"添加kvo之前---setAge:%p %p", [self.per1 methodForSelector:@selector(setAge:)], [self.per2 methodForSelector:@selector(setAge:)]);
        
        //基本使用
        [self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"per1"];
        
        NSLog(@"添加kvo之后---setAge:%p %p", [self.per1 methodForSelector:@selector(setAge:)], [self.per2 methodForSelector:@selector(setAge:)]);
        
        //打断点
        int i = 0;
    }

    //lldb模式:

    2018-12-29 15:05:41.938211+0800 KVONatureSearch[7276:226715] 添加kvo之前---setter:0x1012524a0 0x1012524a0
    2018-12-29 15:05:41.938879+0800 KVONatureSearch[7276:226715] 添加kvo之后---setter:0x1015abcf2 0x1012524a0
    (lldb) p (IMP)0x1012524a0
    (IMP) $0 = 0x00000001012524a0 (KVONatureSearch`-[Person setAge:] at Person.m:13)
    (lldb) p (IMP)0x1015abcf2
    (IMP) $1 = 0x00000001015abcf2 (Foundation`_NSSetIntValueAndNotify)
    (lldb) 

    分析:

    1.per1添加kvo之后,其isa指针指向系统创建的NSKVONotifying_ Person类对象,并复写了父类Person中的setAge:方法,该方法中只有一个方法即_NSSetIntValueAndNotify——该方法是Foundation框架中的方法,是一个C语言函数;

    2.那么,也就是说,kvo的监听方法(- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context)是通过该方法来通知的;

    3.所以问题来了,又是如何通知的呢,又怎么会调用父类的setAge:方法呢?

    ————原因:_NSSetIntValueAndNotify是苹果封装好了的,内部实现无法看到,但是可以通过反编译工具(因为Foundation包编译时会生成)Hopper和命令行的形式加以查看,此处涉及逆向工程、越狱等操作,在此不再赘述,有兴趣的可以自己上网查看!

    经过上述操作:_NSSetIntValueAndNotify的内部实现大概如下:

    //伪代码

    void _NSSetIntValueAndNotify()
    {
        [self willChangeValueForKey:@"age"];
        [super setAge:age];
        [self didChangeValueForKey:@"age"];
    }

    //实现流程验证

    //添加代码

    #import "Person.h"
    
    @implementation Person
    
    - (void)setAge:(int)age
    {
        if (_age != age) {
            _age = age;
        }
        
        NSLog(@"setAge:");
    }
    
    //void _NSSetIntValueAndNotify()
    //{
    //    [self willChangeValueForKey:@"age"];
    //    [super setAge:age];
    //    [self didChangeValueForKey:@"age"];
    //}
    
    - (void)willChangeValueForKey:(NSString *)key
    {
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey");
    }
    
    - (void)didChangeValueForKey:(NSString *)key
    {
        NSLog(@"didChangeValueForKey---begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey---end");
    }
    
    @end

    //打印

    2018-12-29 15:46:27.642672+0800 KVONatureSearch[8236:258015] touchesBegan
    2018-12-29 15:46:27.643455+0800 KVONatureSearch[8236:258015] willChangeValueForKey
    2018-12-29 15:46:27.643583+0800 KVONatureSearch[8236:258015] setAge:
    2018-12-29 15:46:27.643971+0800 KVONatureSearch[8236:258015] didChangeValueForKey---begin
    2018-12-29 15:46:27.644371+0800 KVONatureSearch[8236:258015] 监测到<Person: 0x6000018951d0>的属性age已经改变{
        kind = 1;
        new = 20;
        old = 1;
    }---per1
    2018-12-29 15:46:27.644519+0800 KVONatureSearch[8236:258015] didChangeValueForKey---end

    分析:

    1.willChangeValueForKey:和didChangeValueForKey:是NSObject类的对象方法(类目扩展),用于监测实例对象属性值的改变;

     

    2.我们通过在NSKVONotifying的父类Person中复写上述两个监听方法的结果可知,kvo监听方法的调用是在didChangeValueForKey:方法中进行——那么我们如何验证是不是在该方法中调用的呢,往下看;

    //kvo监听方法调用验证

    //改变代码

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Person : NSObject
    {
        @public
        int _age;
    }
    
    //@property (nonatomic, assign) int age;
    
    @end
    
    NS_ASSUME_NONNULL_END
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        NSLog(@"touchesBegan");
        [self.per1 willChangeValueForKey:@"age"];
    //    self.per1.age = 20;
        self.per1->_age = 20;
    //    [self.per1 didChangeValueForKey:@"age"];
        
    //    self.per1.age = 30;
    }

    说明:

    <1>此处不用@property来定义变量(我们知道,OC对象的本质是结构体,属性即为结构体中的成员,@property关键字在定义成员变量的同时,还为该成员变量在编译阶段由编译器自动生成setter和getter方法,当然如果程序中手动实现了setter和getter方法,则编译器会优先使用而不重新生成setter和getter方法——此处多说了两句,见谅!),而是直接定义成员变量;

    <2>因为系统默认是@protected属性,所以需要设置成@public属性,才能供外部访问;

    <3>此处不走setter方法,改用手动调用;

    //打印

    2018-12-29 15:59:06.901408+0800 KVONatureSearch[8524:267481] touchesBegan
    2018-12-29 15:59:06.901698+0800 KVONatureSearch[8524:267481] willChangeValueForKey

    分析:[self.per1 didChangeValueForKey:@"age"];把该句代码注释后,发现kvo的监听方法并没有触发,那么我们把它打开再看一下;

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        NSLog(@"touchesBegan");
        [self.per1 willChangeValueForKey:@"age"];
    //    self.per1.age = 20;
        self.per1->_age = 20;
        //打开了
        [self.per1 didChangeValueForKey:@"age"];
        
    //    self.per1.age = 30;
    }

    //打印

    2018-12-29 16:16:24.961333+0800 KVONatureSearch[8929:280490] touchesBegan
    2018-12-29 16:16:24.961609+0800 KVONatureSearch[8929:280490] willChangeValueForKey
    2018-12-29 16:16:24.961901+0800 KVONatureSearch[8929:280490] didChangeValueForKey---begin
    2018-12-29 16:16:24.962212+0800 KVONatureSearch[8929:280490] 监测到<Person: 0x600003a10850>的属性age已经改变{
        kind = 1;
        new = 20;
        old = 1;
    }---per1
    2018-12-29 16:16:24.962343+0800 KVONatureSearch[8929:280490] didChangeValueForKey---end

    分析:kvo监听方法触发成功;

    三、结论

    kvo的本质:给实例对象添加观察者时,系统自动创建一个添加前该实例对象所属类的子类NSKVONotifying_Person,该子类通过对父类setter方法的复写(Foundation中封装的方法_NSSetXXXValueAndNotify,由C语言实现)来触发kvo的监听方法(- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context),实现流程为:

    void _NSSetIntValueAndNotify()
    {
        [self willChangeValueForKey:@"age"];
        [super setAge:age];
        [self didChangeValueForKey:@"age"];//kvo的监听方法在该方法中被触发
    }

    //补充:

    1.didChangeValueForKey:和willChangeValueForKey:跟_NSSetIntValueAndNotify()方法一样,是被苹果封装好了的,内部实现无法查看;

    2.所谓的“手动触发kvo”:即直接用实例对象调用上述两个监听方法,值不改变(否则,就直接走setter方法了)

    //代码

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        NSLog(@"touchesBegan");
        [self.per1 willChangeValueForKey:@"age"];
    //    self.per1.age = 20;
    //    self.per1->_age = 20;
        //打开了
        [self.per1 didChangeValueForKey:@"age"];
        
    //    self.per1.age = 30;
    }

    //打印

    2018-12-29 16:33:46.980240+0800 KVONatureSearch[9336:293844] touchesBegan
    2018-12-29 16:33:46.980597+0800 KVONatureSearch[9336:293844] willChangeValueForKey
    2018-12-29 16:33:46.980711+0800 KVONatureSearch[9336:293844] didChangeValueForKey---begin
    2018-12-29 16:33:46.981061+0800 KVONatureSearch[9336:293844] 监测到<Person: 0x6000027b0840>的属性age已经改变{
        kind = 1;
        new = 1;
        old = 1;
    }---per1
    2018-12-29 16:33:46.981204+0800 KVONatureSearch[9336:293844] didChangeValueForKey---end

    GitHub

  • 相关阅读:
    selenium IDE(二)selenium IDE使用
    selenium IDE(一)selenium IDE配置及说明
    自动化一:第一个测试实例
    selenium + python+webdriver+pycharm环境搭建二:pycharm环境自动化测试环境搭建
    selenium + python+webdriver+pycharm环境搭建一:selenium + python自动化测试环境搭建
    前言:学习自动化之前需要知道的
    WSDL
    jmeter练习(5)关联升级版—ForEach控制器(提取多个响应结果并依次传参)
    3P(PS、PR、PDF编辑器Acrobat)中的基基本操作(二)
    3P(PS、PR、PDF编辑器Acrobat)中的基基本操作(一)
  • 原文地址:https://www.cnblogs.com/lybSkill/p/10196775.html
Copyright © 2011-2022 走看看