探究KVO本质

作者: 我是C | 来源:发表于2018-08-08 14:17 被阅读98次

    看了一些资料,对OC更加深入了解,记录总结一下。
    KVO:key-value-boserver,键-值-监听。主要是用来监听对象属性的变化。

    一、KVO 的 简单用法

    首先我们先写一个常规的KVO:
    创建一个Person

    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    @property (nonatomic,assign) NSInteger  age;
    @end
    
    @implementation Person
    @end
    

    创建一个Person 对象p,在touchesBegan中更改p.age的值

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        p = [[Person alloc] init];
        p.age = 10;
            
        [p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:nil];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"observeValueForKeyPath:ofObject:change:context");
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        p.age = 20;
    }
    

    然后点击view,就会触发observeValueForKeyPath监听方法

    二、对象pisa指向

    我们都知道instanceisa 指向 类对象类对象isa 指向元类对象

    所以我们利用函数 object_getClass看看p对象的isa指向,发现:

    添加监听前是isa指向是:Person
    添加监听后是isa指向是:NSKVONotifying_Person

     Class cls = object_getClass(p);//得到p的类对象
     NSLog(@"%@",[cls superclass]);//得到父类
    
    打印:Person
    

    说明对象在添加KVO 的时候,利用runtime 动态生成了一个NSKVONotifying_开头的类,而且该类是被监听类的子类

    如果我们自己手动创建一个NSKVONotifying_Person,会怎么样?
    控制台会有这么一个打印:

    KVO failed to allocate class pair for name NSKVONotifying_Person,
    automatic key-value observing will not work for this class
    

    所以综上所述和验证,可以得到结论:
    在对象添加监听时,runtime会动态生成一个NSKVONotifying_开头的类,并且该类继承自instance对象所属类

    三、KVO的内部实现

    从另一个方向验证:我们可以看下setAge:的IMP的实现

    NSLog(@"1--%p",[p methodForSelector:@selector(setAge:)]);
    [p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:nil];
    NSLog(@"2--%p",[p methodForSelector:@selector(setAge:)]);
    

    打印如下:

    2018-08-08 13:31:54.659785+0800 KVO[47390:1066243] 1--0x10724a610
    2018-08-08 13:31:54.660092+0800 KVO[47390:1066243] 2--0x1076a6bf4
    

    说明方法实现不是在一个类里也再一次证明:添加监听后,runtime 创建了新的类

    我们利用断点进行调试

    image.png
    监听之前:[Person setAge:]
    监听之后:Foundation _NSSetLongLongValueAndNotify
    说明添加监听之后生成的NSKVONotifying_Person对象重写了setAge :这个方法。

    _NSSetLongLongValueAndNotify是Foundation 框架的内部C函数,具体源码需要通过反编译工具去看,不展开讲了。

    伪代码:

    _NSSetLongLongValueAndNotify(){
        [self willChangeValueForKey:@"age"];
        [super setAge:xx];
        [self didChangeValueForKey:@"age"];
    }
    - (void)didChangeValueForKey:(NSString *)key{
        // 通知监听器
        [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
    }
    

    调用didChangeValueForKey内部会调用observeraddObserver:forKeyPath:options:context:方法

    我们可以验证一下上述结论,由于我们不能重写NSKVONotifying_Person的类的方法,所以我们利用继承关系来验证

    @implementation Person
    - (void)setAge:(NSInteger)age{
        NSLog(@"setAge:");
    }
    - (void)willChangeValueForKey:(NSString *)key{
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey:");
    }
    - (void)didChangeValueForKey:(NSString *)key{
        NSLog(@"didChangeValueForKey: -- start");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey: -- end");
    }
    @end
    

    打印如下:

    1.willChangeValueForKey:
    2.setAge:
    3.didChangeValueForKey: -- start
    4.observeValueForKeyPath:ofObject:change:context
    5.didChangeValueForKey: -- end
    

    所以按照这个执行顺序,我们可以验证我们上述伪代码。

    四、KVO的一些细节

    打印出一个类的对象方法

    - (void)methodNameOfClass:(Class)cls{
        unsigned int count;
        
        Method *methodList = class_copyMethodList(cls, &count);
        NSMutableArray *arr = [NSMutableArray array];
        for (int i = 0 ;  i < count; i++) {
            Method method = methodList[I];
            [arr addObject:NSStringFromSelector(method_getName(method))];
        }
        NSLog(@"%@",arr);
        free(methodList);
    }
    

    调用methodNameOfClass

    [self methodNameOfClass:object_getClass(p)];
    

    打印如下:

    (
        "setAge:",
        class,
        dealloc,
        "_isKVOA"
    )
    

    除了setAge :方法还有class,dealloc,_isKVOA
    class:返回类对象
    dealloc:销毁等一系列操作
    _isKVOA:是否是KVO对象,return YES
    值得考究的是class这个函数,我们调用一下这个函数

    [p class]
    

    打印如下:

    Person
    

    为什么是Person
    因为重写了class函数,并将Person类返回,苹果并不想让我们知道添加KVO监听,会动态生成一个新的派生类

    五、如何手动调用KVO

    很简单

    [被监听对象 willChangeValueForKey:key];
    [被监听对象 didChangeValueForKey:key];
    

    如果不写willChangeValueForKey,不会触发监听方法,因为didChangeValueForKey内部会判断willChangeValueForKey之前是否被调用过

    总结

    1 添加KVO时,会利用runtime动态生成一个NSKVONotifying_开头的类。

    2 NSKVONotifying_类继承自被监听的类,也就是instance对象的isa指针指向NSKVONotifying_类,且NSKVONotifying_superClass 指向被监听的类。

    3 NSKVONotifying_类重写了class方法并返回被监听的类

    4 KVO 的本质是重写了set方法,一个对象属性能否被监听,主要是看该属性有没有set方法

    image.png

    相关文章

      网友评论

        本文标题:探究KVO本质

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