美文网首页
KVO的本质分析

KVO的本质分析

作者: SKY_Tang | 来源:发表于2018-09-14 18:32 被阅读0次

    KVO全程就是“key-value observing”,俗称“键值监听”,用于对象属性值改变的监听。

    上代码

    #import "ViewController.h"
    #import "MJPerson.h"
    
    @interface ViewController ()
    
    @property(nonatomic, strong) MJPerson *person1;
    @property(nonatomic, strong) MJPerson *person2;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[MJPerson alloc] init];
        self.person2 = [[MJPerson alloc] init];
        
        self.person1.age = 2;
        self.person2.age = 3;
        
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        self.person1.age = 6;
        self.person2.age = 7;
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    
    @end
    

    点击屏幕改变person1person2age的值,但是只对person1age做了KVO,运行起来看打印结果

    打印结果

    打印看出只有person1age的值改变有打印,但是点击屏幕改变了person1person2age的值,那猜测是不是ageset方法有不同,重写MJPersonsetAge:方法。

    #import "MJPerson.h"
    
    @implementation MJPerson
    
    - (void)setAge:(int)age
    {
        _age = age;
        
        NSLog(@"newAge == %i", _age);
    }
    
    @end
    
    打印结果

    打印看出,点击屏幕改变person1person2age的值,都走了MJPersonsetAge:方法,也就是在setAge:方法上没有什么不同。

    现在我们猜测是不是person1person2instance对象的不同?

    person1age做了KVO前后分别打印person1person2class对象

    #import "ViewController.h"
    #import "MJPerson.h"
    #import <objc/runtime.h>
    
    @interface ViewController ()
    
    @property(nonatomic, strong) MJPerson *person1;
    @property(nonatomic, strong) MJPerson *person2;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[MJPerson alloc] init];
        self.person2 = [[MJPerson alloc] init];
        
        self.person1.age = 2;
        self.person2.age = 3;
        
        NSLog(@"监听前-----person1Class------%@", object_getClass(self.person1));
        NSLog(@"监听前-----person2Class------%@", object_getClass(self.person2));
        
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        
        NSLog(@"监听前-----person1Class------%@", object_getClass(self.person1));
        NSLog(@"监听前-----person2Class------%@", object_getClass(self.person2));
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        self.person1.age = 6;
        self.person2.age = 7;
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    
    @end
    
    打印结果

    打印看出在person1age实现KVO后,person1Class对象改变为NSKVONotifying_MJPerson。也就是KVO时,利用RuntimeAPI动态生成一个NSKVONotifying_MJPerson的类,并且让person1对象的isa指向这个全新的class对象。

    接下来探索NSKVONotifying_MJPersonclass对象有哪些方法实现

    #import "ViewController.h"
    #import "MJPerson.h"
    #import <objc/runtime.h>
    
    @interface ViewController ()
    
    @property(nonatomic, strong) MJPerson *person1;
    
    @end
    
    @implementation ViewController
    
    - (void)printMethodNamesOfClass:(Class)cls
    {
        unsigned int count;
        // 获取方法数组
        Method *methodList = class_copyMethodList(cls, &count);
        // 存储方法名
        NSMutableString *methodNames = [NSMutableString new];
        // 遍历所有方法
        for (int i=0; i<count; i++) {
            // 获得方法
            Method method = methodList[i];
            // 获得方法名
            NSString *methodName = NSStringFromSelector(method_getName(method));
            // 拼接方法名
            [methodNames appendString:methodName];
            [methodNames appendString:@", "];
        }
        // 释放
        free(methodList);
        // 打印方法名
        NSLog(@"%@ %@", cls, methodNames);
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[MJPerson alloc] init];
        
        self.person1.age = 2;
        
        [self printMethodNamesOfClass:object_getClass(self.person1)];
        
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        
        [self printMethodNamesOfClass:object_getClass(self.person1)];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        self.person1.age = 6;
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    
    @end
    
    打印结果

    看打印结果可以看出变化如图

    KVO 前 KVO 后

    通过打印可以看出,KVO的实现监听主要NSKVONotifying_MJPersonclass对象的setAge:方法里,可以用methodForSelector:来打印查看setAge:的实现

    #import "ViewController.h"
    #import "MJPerson.h"
    #import <objc/runtime.h>
    
    @interface ViewController ()
    
    @property(nonatomic, strong) MJPerson *person1;
    
    @end
    
    @implementation ViewController
    
    - (void)printMethodNamesOfClass:(Class)cls
    {
        unsigned int count;
        // 获取方法数组
        Method *methodList = class_copyMethodList(cls, &count);
        // 存储方法名
        NSMutableString *methodNames = [NSMutableString new];
        // 遍历所有方法
        for (int i=0; i<count; i++) {
            // 获得方法
            Method method = methodList[i];
            // 获得方法名
            NSString *methodName = NSStringFromSelector(method_getName(method));
            // 拼接方法名
            [methodNames appendString:methodName];
            [methodNames appendString:@", "];
        }
        // 释放
        free(methodList);
        // 打印方法名
        NSLog(@"%@ %@", cls, methodNames);
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[MJPerson alloc] init];
        
        self.person1.age = 2;
        
    //    [self printMethodNamesOfClass:object_getClass(self.person1)];
        
        NSLog(@"KVO前----%p", [self.person1 methodForSelector:@selector(setAge:)]);
        
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        
    //    [self printMethodNamesOfClass:object_getClass(self.person1)];
        
        NSLog(@"KVO后----%p", [self.person1 methodForSelector:@selector(setAge:)]);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        self.person1.age = 6;
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    
    @end
    

    断点打印

    断点打印

    可以看到KVONSKVONotifying_MJPersonclass对象的setAge:变成了(Foundation _NSSetIntValueAndNotify)Foundation框架的_NSSetIntValueAndNotify函数,因为苹果Foundation没开源,只能用逆向研究。

    越狱手机缓存

    抽取后找到Foundation的编译文件,用工具hopper反编译Foundation的编译文件

    hopper工具搜索 命令搜索

    看到不光有_NSSetIntValueAndNotify,还有其他类型,猜测一下把age换成 double类型是不是NSKVONotifying_MJPersonsetAge:实现相应变为_NSSetDoubleValueAndNotify

    #import <Foundation/Foundation.h>
    
    @interface MJPerson : NSObject
    
    @property(nonatomic, assign) double age;
    
    @end
    
    断点打印

    打印看出把age换成 double类型后,NSKVONotifying_MJPersonsetAge:实现确实相应变为_NSSetDoubleValueAndNotify

    研究NSKVONotifying_MJPersonMJPerson的关系

    通过《isa、superclass的细节》可知,通过结构体mj_objc_class来强制转化class

    #import "ViewController.h"
    #import "MJPerson.h"
    #import <objc/runtime.h>
    
    struct mj_objc_class {
        Class isa;
        Class superclass;
    };
    
    @interface ViewController ()
    
    @property(nonatomic, strong) MJPerson *person1;
    
    @end
    
    @implementation ViewController
    
    - (void)printMethodNamesOfClass:(Class)cls
    {
        unsigned int count;
        // 获取方法数组
        Method *methodList = class_copyMethodList(cls, &count);
        // 存储方法名
        NSMutableString *methodNames = [NSMutableString new];
        // 遍历所有方法
        for (int i=0; i<count; i++) {
            // 获得方法
            Method method = methodList[i];
            // 获得方法名
            NSString *methodName = NSStringFromSelector(method_getName(method));
            // 拼接方法名
            [methodNames appendString:methodName];
            [methodNames appendString:@", "];
        }
        // 释放
        free(methodList);
        // 打印方法名
        NSLog(@"%@ %@", cls, methodNames);
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[MJPerson alloc] init];
        
        self.person1.age = 2;
        
    //    [self printMethodNamesOfClass:object_getClass(self.person1)];
    //    NSLog(@"KVO前----%p", [self.person1 methodForSelector:@selector(setAge:)]);
        
        // 强制转换 KVO 前 person1 的 class 对象
        struct mj_objc_class *person1Class = (__bridge struct mj_objc_class *)(object_getClass(self.person1));
        
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        
    //    [self printMethodNamesOfClass:object_getClass(self.person1)];
    //    NSLog(@"KVO后----%p", [self.person1 methodForSelector:@selector(setAge:)]);
        
        // 强制转换 KVO 后 person1 的 class 对象
        struct mj_objc_class *person1KVOClass = (__bridge struct mj_objc_class *)(object_getClass(self.person1));
        
        NSLog(@"person1Class-------------%p", person1Class);
        NSLog(@"person1KVOClass-------------%p", person1KVOClass);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        self.person1.age = 6;
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    
    @end
    

    断点打印

    断点打印

    打印看到MJPersonNSKVONotifying_MJPersonclass对象不一样。

    断点打印

    打印看到MJPersonNSKVONotifying_MJPersonmeta-class对象不一样。

    断点打印

    打印看到NSKVONotifying_MJPerson的父类是MJPersonMJPerson的父类是NSObject

    根据上面我们可以得到这个逻辑图

    `MJPerson`和`NSKVONotifying_MJPerson`关系图

    现在来研究NSKVONotifying_MJPersonsetAge:内部的实现

    通过hopper能看到_NSSetIntValueAndNotify的实现,下面写一下伪代码

    #import "NSKVONotifying_MJPerson.h"
    
    @implementation NSKVONotifying_MJPerson
    
    - (void)setAge:(int)age
    {
        _NSSetIntValueAndNotify();
    }
    
    void _NSSetIntValueAndNotify()
    {
        [self willChangeValueForKey:@"age"];
        [super setAge:age]
        [self didChangeValueForKey:@"age"]
    }
    
    - (void)didChangeValueForKey:(NSString *)key
    {
        [observer observeValueForKeyPath:key ofObject:self change:nil context:nil];
    }
    
    @end
    

    自定义NSKVONotifying_MJPerson,KVO 的时候会失败报错,所以我们在父类MJPerson中重写方法

    #import "MJPerson.h"
    
    @implementation MJPerson
    
    - (void)setAge:(int)age
    {
        _age = age;
    
        NSLog(@"setAge:");
    }
    
    - (void)willChangeValueForKey:(NSString *)key
    {
        NSLog(@"willChangeValueForKey:-----begin");
        
        [super willChangeValueForKey:key];
        
        NSLog(@"willChangeValueForKey:-----end");
    }
    
    - (void)didChangeValueForKey:(NSString *)key
    {
        NSLog(@"didChangeValueForKey:-----begin");
        
        [super didChangeValueForKey:key];
        
        NSLog(@"didChangeValueForKey:-----end");
    }
    
    @end
    
    #import "ViewController.h"
    #import "MJPerson.h"
    #import <objc/runtime.h>
    
    struct mj_objc_class {
        Class isa;
        Class superclass;
    };
    
    @interface ViewController ()
    
    @property(nonatomic, strong) MJPerson *person1;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[MJPerson alloc] init];
        
        self.person1.age = 2;
        
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        self.person1.age = 6;
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    
    @end
    

    点击屏幕看打印顺序

    点击屏幕打印 `_NSSet*ValueAndNotify`内部实现

    打印顺序可以看出_NSSetXXXValueAndNotify的内部实现

    1. 调用willChangeValueForKey:
    2. 调用原来的setter实现
    3. 调用didChangeValueForKey:

      内部会调用observerobserveValueForKeyPath:ofObject:change:context:方法

    面试题

    iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

    1. 利用RuntimeAPi动态生成一个NSKVONotifying_XXX子类,并且让instance对象的isa指向NSKVONotifying_XXX
    2. 当调用instance对象的set方法时,会调用foundation框架的 _NSSetXXXValueAndNotify函数
      • willChangeValueForKey:
      • 父类的set:方法
      • didChangeValueForKey:

        内部会调用监控器(Observer)的监听方法(observeValueForKeyPath:ofObject:change:context:

    如何手动触发KVO?

    手动调用willChangeValueForKey:didChangeValueForKey:

    直接修改成员变量会触发KVO么?

    不会触发KVO(没有触发set:方法)

    相关文章

      网友评论

          本文标题:KVO的本质分析

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