iOS KVO

作者: 陈盼同学 | 来源:发表于2021-01-26 15:03 被阅读0次

    https://opensource.apple.com/tarballs/objc4/

    由 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) 这题展开了论证

    简单概括为
    当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类
    子类拥有自己的set方法实现,内部会调用
    willChangeValueForKey:
    原来的setter
    didChangeValueForKey:,这个方法内部又会调用监听器(observer)的监听方法

    KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。KVO是观察者模式的又一实现。

    普通KVO使用

    //0首先新建一个MJPerson类,类里有一个age属性。
    #import <Foundation/Foundation.h>
    @interface MJPerson : NSObject
    @property (assign, nonatomic) int age;
    @end
    
    #import "MJPerson.h"
    @implementation MJPerson
    @end
    
    //1.然后在ViewController里初始化MJPerson类
    MJPerson *p1 = [[MJPerson alloc] init];
    p1.age = 1;
    MJPerson *p2 = [[MJPerson alloc] init];
    
    //2.self监听p1的age属性  context主要用来传参,会在监听方法里收到
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    p1.age = 2;
    p2.age = 3;
    
    //3.用完需要移除(在这里p1是个局部,注意移除放置位置)
    [p1 removeObserver:self forKeyPath:@"age"];
    
    //4.监听方法
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"监听到%@的%@改变了 - %@", object, keyPath, change);
    }
    

    疑问,上述里的

    p1.age = 1; //这句话调用了p1的setAge方法
    p1.age = 2; //这句话也只是调用了p1的setAge方法,但是为什么这句话打印了监听方法,上句话就没有打印监听方法呢,可能是运行的时候被动了手脚

    解惑过程

    在添加KVO监听处打断点调试

    (lldb) po p1->isa
    MJPerson
    (lldb) po p2->isa
    MJPerson
    

    在添加KVO监听后打断点调试

    (lldb) po p1->isa
    NSKVONotifying_MJPerson
    (lldb) po p2->isa
    MJPerson
    

    通过打印p1的isa指针(日志栏输入 po p1->isa),可以发现p1.age = 1时isa指针指向MJPerson;添加了kvo后,在p1.age = 2断点时,isa指针指向NSKVONotifying_MJPerson

    通过打印isa指针可以看出添加KVO之后指向了新的子类,现在通过运行时的方法object_getClass也可以证明。

    导入#import <objc/runtime.h>
    然后
    NSLog(@"添加KVO之前 p1 = %@ , p2 = %@ ",object_getClass(p1),object_getClass(p2));
    [p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"添加KVO之后 p1 = %@ , p2 = %@ ",object_getClass(p1),object_getClass(p2));
    可以看到日志栏输出
    2019-11-21 13:08:32.443102+0800 Test[4464:117233] 添加KVO之前 p1 = MJPerson , p2 = MJPerson
    2019-11-21 13:08:32.443342+0800 Test[4464:117233] 添加KVO之后 p1 = NSKVONotifying_MJPerson , p2 = MJPerson
    

    那么NSKVONotifying_MJPerson与MJPerson有什么关系呢?
    其实,NSKVONotifying_MJPerson是MJPerson的子类,添加kvo后,p1实例对象的isa指针指向了NSKVONotifying_MJPerson类对象,NSKVONotifying_MJPerson类对象的superclass指针指向了MJPerson类对象。

    MJPerson类对象里的类结构是

    isa
    superclass
    setAge:
    age
    ...
    

    NSKVONotifying_MJPerson类对象里的类结构是

    isa
    superclass
    setAge:
    class
    dealloc
    isKVOA
    ......
    

    在调用NSKVONotifying_MJPerson类对象的setAge时,本质是调用了Foundation的_NSSetValueAndNotify方法(这是个私有函数)
    我们可以新建一个NSKVONotifying_MJPerson继承MJPerson,在NSKVONotifying_MJPerson里实现_NSSet
    ValueAndNotify过程的伪代码如下

    - (void)setAge:(int)age
    {
        __NSSet*ValueAndNotify();
    }
    
    void __NSSet*ValueAndNotify()  //c语言函数
    {
        [self willChangeValueForKey:@"age"];
        [super setAge:age];
        [self didChangeValueForKey:@"age"];
    }
    
    - (void)didChangeValueForKey:(NSString *)key
    {
        [observer observeValueForKeyPath:@"age" ofObject:self change:@{} context:nil];//didChangeValueForKey又会调用kvo的监听方法
    }
    

    所以整体调用流程是给一个属性实现监听后,isa指针会改变指向,指向了当前类的新生成的一个子类,新生成的子类会重写setAge方法,调用新生成的子类的setAge方法,setAge方法里又会调用c语言的_NSSetValueAndNotify方法,_NSSetValueAndNotify方法里又做了三件事情,[self willChangeValueForKey:@"age"];接下来调用父类的[super setAge:age];,然后[self didChangeValueForKey:@"age"]; ,[self didChangeValueForKey:@"age"]这个方法里又会调用监听器的监听方法- observeValueForKeyPath方法,最后来到了外层控制器的监听方法

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {}
    

    从而监听到age的改变

    备注:如果添加了kvo后,当前实例对象的isa就一直指向新的子类,如果当前实例对象还有其他属性,不会重写其他属性的set方法。如果设置其他属性等操作,会从子类开始找不到去父类。

    那么现在来验证下上面的伪代码是否真的是这样的

    先来验证方法实现不一样(methodForSelector就是你传一个方法给我,我给你找到实现,这个方法返回类型是IMP)
    IMP在源码里这样定义的
    typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); //语法的意思是IMP是一个指向函数的指针,该函数返回id并将id,SEL等作为参数
    这样说来,IMP是一个指向函数的指针,这个被指向的函数包括id(“self”指针),调用的SEL(方法名),再加上一些其他参数。

    NSLog(@"添加KVO之前 p1 =  %p , p2 = %p",[p1 methodForSelector:@selector(setAge:)],[p2 methodForSelector:@selector(setAge:)]);
    [p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"添加KVO之后 p1 =  %p , p2 = %p",[p1 methodForSelector:@selector(setAge:)],[p2 methodForSelector:@selector(setAge:)]);
    可以看到日志栏输出
    2019-11-21 13:36:55.645116+0800 Test[4792:134131] 添加KVO之前 p1 =  0x105a1c000 , p2 = 0x105a1c000
    2019-11-21 13:36:55.645338+0800 Test[4792:134131] 添加KVO之后 p1 =  0x7fff25701c8a , p2 = 0x105a1c000
    
    然后我们在xcode日志栏输入打印指令(将IMP指针的地址在控制台转成IMP输出,看看具体值)
    (lldb) p (IMP)0x105a1c000
    (IMP) $0 = 0x0000000105a1c000 (Test`-[MJPerson setAge:] at MJPerson.h:14)
    (lldb) p (IMP)0x7fff25701c8a
    (IMP) $1 = 0x00007fff25701c8a (Foundation`_NSSetIntValueAndNotify)//(_NSSetIntValueAndNotify中的Int是因为我们定义的age是int类型的)
    可以看出添加kvo后确实改变了调用
    

    监听属性是int时,方法为_NSSetIntValueAndNotify
    监听属性是double时,方法为_NSSetDoubleValueAndNotify
    监听属性是NSString时,方法为_NSSetObjectValueAndNotify
    所以_NSSet*ValueAndNotify这个函数就是添加kvo后调用的

    NSKVONotifying_MJPerson类对象里的类结构其他东西是干嘛的呢

    //首先写一个运行时,用来打印一个类的对象方法
    - (void)printMethods:(Class)cls
    {
        unsigned int count;
        Method *methods = class_copyMethodList(cls, &count);
        
        NSMutableString *methodNames = [NSMutableString string];
        [methodNames appendFormat:@"%@ - ", cls];
        
        for (int i = 0; i < count; i++) {
            Method method = methods[i];
            NSString *methodName = NSStringFromSelector(method_getName(method));
            [methodNames appendString:methodName];
            [methodNames appendString:@" "];
        }
        
        NSLog(@"%@", methodNames);
        
        free(methods);//C语言里,methods通过copy出来的,所以要手动释放
    }
    
    //创建两个MJPerson对象
    MJPerson *p1 = [[MJPerson alloc] init];
    p1.age = 1;
    
    MJPerson *p2 = [[MJPerson alloc] init];
    p2.age = 2;
    
    // self监听p1的age属性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    
    //调用打印类的对象方法
    [self printMethods:object_getClass(p2)];
    [self printMethods:object_getClass(p1)];
    
    运行程序,可以看到打印
    日志输出:  MJPerson - setAge: age
    日志输出:  NSKVONotifying_MJPerson - setAge: class dealloc _isKVOA
    

    由此可以看出NSKVONotifying_MJPerson类对象里的类结构和MJPerson类结构

    NSKVONotifying_MJPerson类对象里的类结构

    isa     //指向元类对象
    superclass   //指向MJPerson类对象
    setAge:      //_NSSet*ValueAndNotify
    class       // 重写了NSKVONotifying_MJPerson类的实现方法,为了不把这个类暴露出去,效果是return class_getSuperclass(object_getClass(self));不然如果不重写,我们调用[p1 class]时,一步步找到了NSObject,大致效果是NSObject里return object_getClass(self)最后把NSKVONotifying_MJPerson类给返回了出去,这不是苹果想看到的。上文中的p1,p2,我们打印[p1 class]和[p2 class]时,返回的都是MJPerson,这样外界就不知道NSKVONotifying_MJPerson存在。不过,如果我们打印object_getClass(p1)这句话,等同于我们打印了p1->isa,输出的是NSKVONotifying_MJPerson类
    dealloc     //  大致是释放kvo吧
    isKVOA      //不知道
    ......
    

    由 如何手动触发KVO?这题展开了论证

    题目意思大致就是 如何在不修改值的前提下触发KVO监听方法
    答案
    手动调用willChangeValueForKey:和didChangeValueForKey:

    比如
    // 首先要添加了KVO,然后想要手动触发KVO,就写下面两句,必须两句一起用
    [p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
    // 然后想要手动触发KVO,就写下面两句,必须两句一起用
    [p1 willChangeValueForKey:@"age"];
    [p1 didChangeValueForKey:@"age"]; //真正触发监听方法
    

    一直在 说didChangeValueForKey才是触发监听,以及 kvo内部实现是

    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
    那么怎么论证呢

    //首先在MJPerson.m文件里下实现setAge和willChangeValueForKey和didChangeValueForKey
    
    #import "MJPerson.h"
    
    @implementation MJPerson
    
    - (void)setAge:(int)age
    {
        NSLog(@"setAge:");
        _age = age;
    }
    
    - (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
    
    //然后在ViewController控制器里实现下列
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        MJPerson *p1 = [[MJPerson alloc] init];
        p1.age = 1;
        
        MJPerson *p2 = [[MJPerson alloc] init];
        p2.age = 2;
        
        // self监听p1的age属性
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        p1.age = 20;
        [p1 removeObserver:self forKeyPath:@"age"];
    }
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"监听到%@的%@改变了 - %@", object, keyPath, change);
    }
    
    //可以看到日志台输出
    2018-12-26 11:46:08.234305+0800 Interview05-KVO[2750:106490] setAge:
    2018-12-26 11:46:08.234468+0800 Interview05-KVO[2750:106490] setAge:
    2018-12-26 11:46:08.234917+0800 Interview05-KVO[2750:106490] willChangeValueForKey: - begin
    2018-12-26 11:46:08.235120+0800 Interview05-KVO[2750:106490] willChangeValueForKey: - end
    2018-12-26 11:46:08.235237+0800 Interview05-KVO[2750:106490] setAge:
    2018-12-26 11:46:08.235343+0800 Interview05-KVO[2750:106490] didChangeValueForKey: - begin
    2018-12-26 11:46:08.235616+0800 Interview05-KVO[2750:106490] 监听到<MJPerson: 0x60400001ee40>的age改变了 - {
        kind = 1;
        new = 20;
        old = 1;
    }
    2018-12-26 11:46:08.235762+0800 Interview05-KVO[2750:106490] didChangeValueForKey: - end
    
    

    由 直接修改成员变量会触发KVO吗?这题展开了论证

    不会触发。因为KVO本质是修改set方法,只有调用set才会触发。

    //MJPerson类的定义一个成员变量
    @interface MJPerson : NSObject
    {
    @public
        int _age;
    }
    @end
    
    //然后在ViewController控制器里实现下列
    @interface ViewController ()
    @property (nonatomic ,strong) MJPerson *pp;
    @end
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.pp = [[MJPerson alloc] init];
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.pp addObserver:self forKeyPath:@"age" options:options context:@"123"];
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.pp->_age = 2;
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"监听到%@的%@改变了 - %@", object, keyPath, change);
    }
    
    -(void)dealloc {
        [self.pp removeObserver:self forKeyPath:@"age"];
    }
    @end
    

    成员变量是什么

    https://www.cnblogs.com/yumian/p/5123596.html
    成员变量是定义在{ }号中的变量,如果变量的数据类型是一个类则称这个变量为实例变量。
    成员变量不会自动生成set和get方法,需要自己手动实现。
    访问成员变量可以通过self->成员变量名字,也可以直接通过变量名字访问。

    虽然直接修改成员变量不会触发KVO

    #import <Foundation/Foundation.h>
    @interface MJPerson : NSObject{
        @public
        int _age;
    }
    @end
    #import "MJPerson.h"
    
    @implementation MJPerson
    @end
    控制器里
    - (void)viewDidLoad {
        [super viewDidLoad];
        MJPerson *p1 = [[MJPerson alloc]init];
        p1->_age = 1;
        [p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
        p1->_age = 2; //不会触发KVO
    }
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"---%@",change);
    }
    

    但是手动实现成员变量set方法,通过set赋值还是能触发kvo的。但是就算实现了set和get,如果直接调用成员变量(对象->成员变量),还是不能触发。

    #import <Foundation/Foundation.h>
    @interface MJPerson : NSObject{
        @public
        int _age;
    }
    -(void)setAge:(int)a;
    - (int)age;
    @end
    
    #import "MJPerson.h"
    @implementation MJPerson
    -(void)setAge:(int)a{
        _age = a;
    }
    - (int)age{
        return _age;
    }
    @end
    控制器里
    - (void)viewDidLoad {
        [super viewDidLoad];
        MJPerson *p1 = [[MJPerson alloc]init];
        [p1 setAge:1];
        [p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
        [p1 setAge:2]; //触发KVO
        p1.age = 3;    //触发KVO ,这个p.age跟get方法命名的age没有任何关系,拿的是成员变量的age去走set方法
        p1->_age = 4;  //不会触发KVO
    }
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"---%@",change);
    }
    

    相关文章

      网友评论

          本文标题:iOS KVO

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