美文网首页
【iOS】KVO的实现

【iOS】KVO的实现

作者: BeethOven | 来源:发表于2020-05-08 08:40 被阅读0次

    KVO:(Key-Value-Observering)键值监听,用于监听某个对象属性的变化

    键值监听

    KVO的基本使用

    #import "ViewController.h"
    #import "MJPerson.h"
    
    @interface ViewController ()
    @property (strong, nonatomic) MJPerson *person1;
    @property (strong, nonatomic) MJPerson *person2;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[MJPerson alloc] init];
        self.person1.age = 1;
        self.person1.height = 11;
        
        self.person2 = [[MJPerson alloc] init];
        self.person2.age = 2;
        self.person2.height = 22;
        
        // 给person1对象添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        [self.person1 addObserver:self forKeyPath:@"height" options:options context:@"456"];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        self.person1.age = 20;
        self.person2.age = 20;
        
        self.person1.height = 30;
        self.person2.height = 30;
    }
    
    - (void)dealloc {
        [self.person1 removeObserver:self forKeyPath:@"age"];
        [self.person1 removeObserver:self forKeyPath:@"height"];
    }
    
    // 当监听对象的属性值发生改变时,就会调用
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    
    

    打印pesrson实例的isa指针,会发现添加监听的实例对象,不指向MJPerson类对象,而是指向NSKVONotifying_MJPerson,NSKVONotifying_MJPerson是属于MJPerson的子类。

    (lldb) p self.person1 -> isa
    (Class) $2 = NSKVONotifying_MJPerson
    (lldb) p self.person2 -> isa
    (Class) $3 = MJPerson
    

    NSKVONotifying_MJPerson实现伪代码, set方法调用Foundation的c代码_NSSetIntValueAndNotify()函数

    - (void)setAge:(int)age
    {
        _NSSetIntValueAndNotify();
    }
    
    // 伪代码
    void _NSSetIntValueAndNotify()
    {
        [self willChangeValueForKey:@"age"];
        [super setAge:age];
        [self didChangeValueForKey:@"age"];
    }
    
    - (void)didChangeValueForKey:(NSString *)key
    {
        // 通知监听器,某某属性值发生了改变
        [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
    }
    
    

    使用KVO和未使用KVO监听的对象的区别

    • NSKVONotifying_MJPerson和MJPerson拥有对象方法:age的set和get方法
    • NSKVONotifying_MJPerson的superclass指针指向MJPerson,其class方法返回MJPerson类对象
    未使用KVO监听的对象 使用KVO监听的对象

    观察对象使用KVO后变化

    isa指向类对象
        self.person1 = [[MJPerson alloc] init];
        self.person1.age = 1;
        
        self.person2 = [[MJPerson alloc] init];
        self.person2.age = 2;
        
        NSLog(@"person1添加KVO监听之前 - %@ %@",
              object_getClass(self.person1),
              object_getClass(self.person2));
        NSLog(@"person1添加KVO监听之前 - %p %p",
              [self.person1 methodForSelector:@selector(setAge:)],
              [self.person2 methodForSelector:@selector(setAge:)]);
              
        // 给person1对象添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        
        NSLog(@"person1添加KVO监听之后 - %@ %@",
              object_getClass(self.person1),
              object_getClass(self.person2));
        NSLog(@"person1添加KVO监听之后 - %p %p",
              [self.person1 methodForSelector:@selector(setAge:)],
              [self.person2 methodForSelector:@selector(setAge:)]);
    

    打印结果:

    2020-05-06 22:29:04.845450+0800 Interview01[82395:1472694] person1添加KVO监听之前 - MJPerson MJPerson
    2020-05-06 22:29:04.845547+0800 Interview01[82395:1472694] person1添加KVO监听之前 - 0x10c5864d0 0x10c5864d0
    2020-05-06 22:29:04.845854+0800 Interview01[82395:1472694] person1添加KVO监听之后 - NSKVONotifying_MJPerson MJPerson
    2020-05-06 22:29:04.845975+0800 Interview01[82395:1472694] person1添加KVO监听之后 - 0x7fff25623f0e 0x10c5864d0
    

    设置KVO之后person1的类对象变成了NSKVONotifying_MJPerson,调用的方法打印值也不一样了。
    lldb调试打印方法地址值, 这里的KVO方法发现其实调用的Foundation框架的_NSSetIntValueAndNotify方法(这里的不一定是_NSSetIntValueAndNotify,可能是_NSSetDoubleValueAndNotify等)

    (lldb) p (IMP)0x7fff25623f0e
    (IMP) $1 = 0x00007fff25623f0e (Foundation`_NSSetIntValueAndNotify)
    (lldb) p (IMP)0x10b380510
    (IMP) $2 = 0x000000010b380510 (Interview01`-[MJPerson setAge:] at MJPerson.m:13)
    

    类对象的方法解析

    #import <objc/runtime.h>
    #import "MJPerson.h"
    
    - (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);
    }
    
     - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[MJPerson alloc] init];
        self.person1.age = 1;
        
        self.person2 = [[MJPerson alloc] init];
        self.person2.age = 2;
        
        // 给person1对象添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        
        [self printMethodNamesOfClass:object_getClass(self.person1)];
        [self printMethodNamesOfClass:object_getClass(self.person2)];
    }
    
    

    打印结果

    2020-05-08 08:14:12.932361+0800 Interview01[91269:1963035] NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
    2020-05-08 08:14:12.932498+0800 Interview01[91269:1963035] MJPerson age, setAge:,
    
    

    NSKVONotifying_MJPerson返回4个对象方法
    // 伪代码如下

    - (void)setAge:(int)age
    {
        _NSSetIntValueAndNotify();
    }
    
    // 屏幕内部实现,隐藏了NSKVONotifying_MJPerson类的存在
    - (Class)class
    {
        return [MJPerson class];
    }
    - (void)dealloc
    {
        // 收尾工作
    }
    
    - (BOOL)_isKVOA
    {
        return YES;
    }
    
    

    其中重写class方法实现可以通过代码观察到

    NSLog(@"person1:%@, person2:%@", object_getClass(self.person1), object_getClass(self.person2));
    NSLog(@"person1:%@, person2:%@", [self.person1 class], [self.person1 class]);
    

    打印值

    2020-05-08 08:24:29.546611+0800 Interview01[91368:1969399] person1:NSKVONotifying_MJPerson, person2:MJPerson
    2020-05-08 08:24:29.546762+0800 Interview01[91368:1969399] person1:MJPerson, person2:MJPerson
    

    所以推测出这里class实现是屏幕内部实现,隐藏了NSKVONotifying_MJPerson类的存在。

    总结

    iOS利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类。当修改对象instance属性时,会调用Foundation的_NSSetXXXValueAndNotify函数。伪代码实现

    • willChangeValueForKey
    • 父类的Set方法
    • didChangeValueForKey
      内部触发监听器(Oberser)的监听方法(observerValueForKeyPath: ofObject:change:context)

    相关文章

      网友评论

          本文标题:【iOS】KVO的实现

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