KVO本质

作者: dandelionYD | 来源:发表于2019-01-08 12:45 被阅读48次

    面试题:

    1.ios是用什么方式实现对一个对象的KVO?(KVO的本质是啥)
    析:利用runtimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
        当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
        - willChangeValueForKey;
        - 父类原来的setter
        - didChangeValueForKey;
        内部会触发监听器(Observer)的监听方法(observeValueForKeyPath:ofObjectchange:context)
    
    2.如何手动的触发KVO?
    析:手动调用willChangeValueForKey和didChangeValueForKey;
     
    3直接修改成员变量会 触发KVO?
     不会触发
    

    简单使用:

    #import "ViewController.h"
    
    @interface Person : NSObject{
        @public
        int _age;
    }
    @property (nonatomic,strong)NSString  *name;
    @property (nonatomic,assign)CGFloat  height;
    @end
    
    @implementation Person
    @end
    
    
    @interface ViewController ()
    @property (nonatomic,strong)Person  *person1;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.person1 = [[Person alloc]init];
        self.person1.name = @"Jack";
        self.person1.height  = 175;
        self.person1->_age = 18;
       
         //添加KVO监听
         NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
         [self.person1 addObserver:self forKeyPath:@"height" options:options context:@"height-contenxt"];
         [self.person1 addObserver:self forKeyPath:@"name" options:options context:@"name-contenxt"];
         [self.person1 addObserver:self forKeyPath:@"_age" options:options context:@"_age-contenxt"];
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.person1.name = @"Jack2";
        self.person1.height = 180;
        self.person1->_age = 20;
    }
    
    // 当监听对象的属性值发生改变时,就会调用
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
         NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    
    - (void)dealloc {
        [self.person1 removeObserver:self forKeyPath:@"height"];
        [self.person1 removeObserver:self forKeyPath:@"name"];
    }
    @end
    
    
    控制台Log:
    ======================
    监听到<Person: 0x600003962700>的name属性值改变了 - {
        kind = 1;
        new = Jack2;
        old = Jack;
    } - name-contenxt
    监听到<Person: 0x600003962700>的height属性值改变了 - {
        kind = 1;
        new = 180;
        old = 175;
    } - height-contenxt
    

    发现:直接修改成员变量会 是不会触发KVO的.
    //分析:NSDictionary<NSKeyValueChangeKey,id> *)

    • new:新值

    • old:旧值

    • kind(指明了变更的类型):(NSKeyValueChange-->NSKeyValueChangeKindKey)

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

      一般情况下返回的都是1也就是第一个NSKeyValueChangeSetting,但是如果你监听的属性是一个集合对象的话,当这个集合中的元素被插入,删除,替换时,就会分别返回NSKeyValueChangeInsertion,NSKeyValueChangeRemoval和NSKeyValueChangeReplacement(有兴趣的可以去试试哟)


    下面来一步步分析:

    1.首先新建一个myPerson类如下:

    //.h
    #import <Foundation/Foundation.h>
    @interface myPerson : NSObject
    @property (nonatomic,strong)NSString  *name;
    @end
    
    //.m
    #import "myPerson.h"
    @implementation myPerson
    -(void)willChangeValueForKey:(NSString *)key{
        NSLog(@"%s-%d",__FUNCTION__,__LINE__);
        [super willChangeValueForKey:key]
    }
    
    -(void)didChangeValueForKey:(NSString *)key{
         NSLog(@"%s-%d",__FUNCTION__,__LINE__);
        [super didChangeValueForKey:key]
    }
    @end
    
    //viewcontroller.m
    #import "ViewController2.h"
    #import "myPerson.h"
    #import <objc/runtime.h>
    
    @interface ViewController2 ()
    @property (nonatomic,strong)myPerson  *p;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.title = @"KVO本质";
        
        self.p  = [[myPerson alloc]init];
        self.p.name = @"jack";
        
        NSLog(@"添加之前“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
        NSLog(@"添加之前的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);
    
        //添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.p addObserver:self forKeyPath:@"name" options:options context:@"name-contenxt"];
        
        NSLog(@"添加之后“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
        NSLog(@"添加之后的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);
        NSLog(@"End");
        
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        [self.p  setName:@"Jack2"];
        
    }
    
    // 当监听对象的属性值发生改变时,就会调用
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    
    - (void)dealloc {
        [self.p removeObserver:self forKeyPath:@"name"];
        NSLog(@"%s-%d",__FUNCTION__,__LINE__);
    }
    @end
    

    分析:在 NSLog(@"End");处打一个断点
    打印log日志:

    添加之前“类(isa)“
    myPerson
    添加之前的name的set方法
    0x10290c5f0
    添加之后“类(isa)“
    NSKVONotifying_myPerson
    添加之后的name的set方法
    0x102c6663a
    

    我们可以得出:

    • 1.在添加kvo监听之前class对象为:myPerson、在添加kvo监听之后class对象为NSKVONotifying_myPerson
    • 2.在添加kvo监听之前name的set方法:0x10290c5f0、在添加kvo监听之后name的set方法:0x102c6663a
      (不一样)
    • 3.继续验证
    输入:p (IMP)0x10ec655f0   
    结果:(IMP) $0 = 0x000000010ec655f0 (KVO`-[myPerson setName:] at myPerson.h:14)
    输入:p (IMP)0x10efbf63a 
    结果:(IMP) $1 = 0x000000010efbf63a (Foundation`_NSSetObjectValueAndNotify)
    

    我们猜测:
    (1).在添加之后:底层会动态的创建一个myPerson的子类(继承了类myPerson), NSKVONotifying_myPerson
    (2).然后会去重新实现setName的方法

    • 4.会执行-[myPerson willChangeValueForKey:]-15
    • 5.会执行-[myPerson didChangeValueForKey:]-19

    2.我们在修改下viewcontroller文件

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.title = @"KVO本质";
        
        self.p  = [[myPerson alloc]init];
        self.p.name = @"jack";
        
        NSLog(@"添加之前“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
        NSLog(@"添加之前的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);
        [self printMethodNamesOfClass:object_getClass(self.p)];
        
        //添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.p addObserver:self forKeyPath:@"name" options:options context:@"name-contenxt"];
    
        NSLog(@"添加之后“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
        NSLog(@"添加之后的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);
        [self printMethodNamesOfClass:object_getClass(self.p)];
        
        NSLog(@"End");
        
    }
    
    //利用runtime来打印类的属性和方法值
    - (void)printMethodNamesOfClass:(Class)cls
    {
        unsigned int count;
        // 获得方法数组
        Method *methodList = class_copyMethodList(cls, &count);
        // 存储方法名
        NSMutableString *methodNames = [NSMutableString string];
        
        // 遍历所有的方法
        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);
    }
    

    发现:

     [self printMethodNamesOfClass:object_getClass(self.p)];第一次:
     myPerson .cxx_destruct, name, setName:,
     
     [self printMethodNamesOfClass:object_getClass(self.p)];第二次:
     NSKVONotifying_myPerson setName:, class, dealloc, _isKVOA,
     
    在NSKVONotifying_myPerson里面多了setName、class、dealloc, _isKVOA的方法
    

    我们来写一个:

    //NSKVONotifying_myPerson_Test.h
    #import "myPerson.h"
    @interface NSKVONotifying_myPerson_Test : myPerson
    @end
    
    #import "NSKVONotifying_myPerson_Test.h"
    @implementation NSKVONotifying_myPerson_Test
    -(void)setName:(NSString *)name{
        _NSSetObjectValueAndNotify();
    }
    
    void _NSSetObjectValueAndNotify(){
        //里面做了一些列的处理
    }
    
    //内部实现,隐藏了NSKVONotifying_myPerson类的存在
    -(Class)class{
       return [myPerson class];
    }
    
    -(void)dealloc{
        //释放相应的资源
    }
    
    - (BOOL)_isKVOA
    {
        return YES;
    }
    @end
    
    [self printMethodNamesOfClass:[NSKVONotifying_myPerson_Test class]];
    打印:NSKVONotifying_myPerson_Test _isKVOA, dealloc, setName:, class,
    

    分析:

    1. dealloc、_isKVOA方法:return YES;(显然可知)
    2. (Class)class方法:return [myPerson class]; (屏幕内部实现,隐藏了NSKVONotifying_myPerson类的存在)

    我们在来看看:_NSSetObjectValueAndNotify的里面具体的逻辑:

    • 1.从多了setName方法,我们可以知道,它肯定重新写了setName的方法,而在原来的myPerson对象,监听的方法同样走了willChangeValueForKey和didChangeValueForKey方法 ,所以它肯定也调用了super的方法;
    • 2.我们在来打个断点看看,看看栈的执行函数
      //将断点打在:touches方法处,然后在控制台输入:
      watchpoint set variable _p->_name(注意这里只能使用_的成员变量)
      
      image

    点击stepOver(上绿色按钮)会发现:

    image image image

    那么我们可以猜测: _NSSetObjectValueAndNotify的方法的简单内部实现

    void _NSSetObjectValueAndNotify(){
      [self willChangeValueForKey:@"name"];
      [super setName:name];
      [self didChangeValueForKey:@"name"];
     }
     
     - (void)didChangeValueForKey:(NSString *)key{
      [observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
     }
    

    综上所述:我们可以得出下关系图:


    image
    image

    说明:

    • 调用addObserve的方法的时候,苹果会动态的生成一个继承了myPerson的子类NSKVONotifying_myPerson,

    • 在NSKVONotifying_myPerson内部会重新实现setName的方法,setName方法里面的内部实现是一个C函数

    • 该C函数内部的实现大致的为,先实现willChangeValueForKey,然后实现父类的setName的方法,然后实现didChangeValueForKey,里面在通知监听者,该属性变化了

    • 在NSKVONotifying_myPerson也添加了一个isKVO的方法.标记是KVO使用的类

    • 在NSKVONotifying_myPerson也添加了一个dealloc的方法,来释放不用的资源

    • 在NSKVONotifying_myPerson也添加了一个class的方法(return [myPerson class]),屏蔽了系统的内部实现(但是这里不是isa直接指向myPerson的元类对象,证明见下)

    #import "ViewController3.h"
    #import "myPerson.h"
    #import <objc/runtime.h>
    
    struct my_class{
        Class isa;
    };
    
    @interface ViewController3 ()
    @property (nonatomic,strong)myPerson  *p;
    @end
    
    @implementation ViewController3
    
    - (void)viewDidLoad {
        [super viewDidLoad];
       
        //0x0000000ffffffff8ULL  0x00007ffffffffff8ULL
        
        self.p  = [[myPerson alloc]init];
        self.p.name = @"jack";
        
        NSLog(@"添加之前“类(isa)“\n%p",object_getClass(self.p)); //self.p->isa
        NSLog(@"添加之前“元类”\n%p",object_getClass(object_getClass(self.p)));
        Class class = object_getClass(self.p);
        Class mclass = object_getClass(object_getClass(self.p));
        struct my_class  *myC = (__bridge struct my_class *)class;
        struct my_class *mMetaC = (__bridge struct my_class *)mclass;
        
       
        //添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.p addObserver:self forKeyPath:@"name" options:options context:@"name-context"];
        
        NSLog(@"添加之后“类(isa)“\n%p",object_getClass(self.p)); //self.p->isa
        NSLog(@"添加之后“元类”\n%p",object_getClass(object_getClass(self.p)));
        
        Class class2 = object_getClass(self.p);
        Class mclass2 = object_getClass(object_getClass(self.p));
        struct my_class  *myC2 = (__bridge struct my_class *)class2;
        struct my_class *mMetaC2 = (__bridge struct my_class *)mclass2;
      
        NSLog(@"End");
        
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.p.name = @"Jack2";
        
    }
    
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    
    - (void)dealloc {
        [self.p removeObserver:self forKeyPath:@"name"];
        NSLog(@"%s-%d",__FUNCTION__,__LINE__);
    }
    
    @end
    

    在 NSLog(@"End");打个断点:


    image

    从里面的内存值:很容易看出,素虽然返回了return [myPerson class],但是它的isa这里不是指向myPerson的元类对象。而是自己的元类的对象哟


    我们试试看手动写一个KVO

    NSObject+KVO.h
    #import <Foundation/Foundation.h>
    
    @interface NSObject (KVO)
    //给NSObject类添加一个监听的方法 
    -(void)my_ddObserver:(NSObject*)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    @end
    
    NSObject+KVO.m
    #import "NSObject+KVO.h"
    #import <objc/runtime.h>
    #import <objc/message.h>
    
    
    @implementation NSObject (KVO)
    -(void)my_ddObserver:(NSObject*)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
        //动态添加一个类
        NSString *originClassName = NSStringFromClass([self class]);
        
        //苹果创建的是 NSKVONotifying_  为前缀的
        NSString *newClassName = [@"myKVONotifying_" stringByAppendingString:originClassName];
        
        
        const char *newName = [newClassName UTF8String];
        
        // 继承自当前类,创建一个子类
        Class kvoClass = objc_allocateClassPair([self class], newName, 0);
        
        // 添加setter方法
        class_addMethod(kvoClass, @selector(setName:), (IMP)setName, "v@:@@");
        
        //注册新添加的这个类
        objc_registerClassPair(kvoClass);
        
        // 修改isa,本质就是改变当前对象的类名
        object_setClass(self, kvoClass);
        
        // 保存观察者属性到当前类中
        objc_setAssociatedObject(self, (__bridge const void *)@"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        
    }
    
    #pragma mark - 重写父类方法
    void setName(id self, SEL _cmd, NSString *name) {
        
        // 保存当前KVO的类
        Class kvoClass = [self class];
        
        // 将self的isa指针指向父类myPerson,调用父类setter方法
        object_setClass(self, class_getSuperclass([self class]));
        
    #warning -- 这边方法会报错:build Settings 里面搜索enable strict 修改为NO即可
        // 调用父类setter方法,重新复制
        objc_msgSend(self, @selector(setName:), name);
        
        // 取出myKVO_Person观察者
        id objc = objc_getAssociatedObject(self, (__bridge const void *)@"observer");
    
        
        // 通知观察者,执行通知方法
        //这里是直接重新写的setName方法,所以很容易知道是 “name”
        objc_msgSend(objc, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"kind":@"kind_value",@"new":@"new_value",@"old":@"old_value"}, @"这里是监听的上下文环境");
        
        // 重新修改为myKVO_Person类
        object_setClass(self, kvoClass);
        
    }
    @end
    
    
    实现:
    #import "ViewController4.h"
    #import "NSObject+KVO.h"
    #import "myPerson.h"
    
    @interface ViewController4 ()
    @property (nonatomic,strong)myPerson  *p;
    @end
    
    @implementation ViewController4
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.title  = @"手动写一个KVO";
        self.p = [myPerson new];
        self.p.name = @"jack";
        
        [self.p my_ddObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:@"123"];
    }
    
    - (void)dealloc {
        [self.p removeObserver:self forKeyPath:@"name"];
        NSLog(@"%s-%d",__FUNCTION__,__LINE__);
    }
    
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.p.name =  @"Lucy";
    }
    @end
    

    友情链接:

    相关文章

      网友评论

        本文标题:KVO本质

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