美文网首页
Objective-C KVO总结

Objective-C KVO总结

作者: 番茄炒西红柿啊 | 来源:发表于2020-09-15 11:17 被阅读0次

    看本章之前建议先打好OC对象的基础:深入理解OC中的对象

    大纲

    • 什么是KVO
    • 场景代码示例 (后面的分析都是基于示例代码展开)
    • NSKVONotifying_XXX 类对象
    • _NSSetXXXValueAndNotify 方法
    • 自定义手动触发KVO
    • KVO总结
    • 拓展补充

    1. 什么是KVO

    KVO的全称是Key-Value Observing,俗称“键值监听”。可以用于监听某个对象属性值的改变。

    • KVO使用:
      1. 确定需要监听的对象的属性。
      2. 确定监听者,并注册监听回调。
      3. 在监听回调中处理业务逻辑。
    KVO流程图

    2. 场景代码示例

    定义一个类,申明一个count属性

    @interface KVOTest : NSObject
    @property (nonatomic, assign) NSInteger count;
    @end
    
    @implementation KVOTest
    @end
    

    viewcontroller中创建对象,注册属性监听。点击屏幕的时候改变count值

    static void *countContext = &countContext;
    
    @interface ViewController ()
    @property (nonatomic, strong) KVOTest   *kvo;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.kvo = [[KVOTest alloc] init];
        self.kvo.count = 1;
        // 注册监听
        [self.kvo addObserver:self
                   forKeyPath:@"count"
                      options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                      context:countContext];
    }
    
    // 监听回调
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        if (context == countContext)
        {
            NSInteger newCount = [[change valueForKey:NSKeyValueChangeNewKey] integerValue];
            NSInteger oldCount = [[change valueForKey:NSKeyValueChangeOldKey] integerValue];
            NSLog(@"new: %ld, old: %ld", newCount, oldCount);
        }
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        // 点击屏幕,改变count的值
        self.kvo.count += 1;
    }
    @end
    

    点击屏幕的时候修改count的值将会触发监听回调,控制台打印如下:



    上面就是个最简单的KVO使用的场景示例了。

    3. NSKVONotifying

    我们回到上面的示例代码中添加如下代码:输出注册监听前后类对象类对象对应的地址

    我们可以先猜测一下控制台的log信息,如果你只是停留在单纯使用KVO的阶段,应该会和我当初猜想的一致。注册监听前后log的“类对象”和“地址”应该都是一样的。而实际的结果是这样的: 是不是很惊讶,我们创建的KVOTest实例对象的属性被注册了KVO监听后,苹果底层其实偷偷的做了修改,使用runtime运行时动态的创建了一个新的类NSKVONotifying_KVOTest.通过对比类对象的地址0x102ac2a980x600001f8cb40也可以确认的确不是同一个类。大致流程如下:
    1. 我们先验证属性监听isa的变化:
    • 监听后isa的确发生了变化,所以类对象的确发生了改变。
    1. 验证NSKVONotifying_KVOTest的父类是否是KVOTest
    struct cw_objc_class {
        Class _Nonnull isa;
        Class _Nullable super_class;
    };
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.kvo = [[KVOTest alloc] init];
        self.kvo.count = 1;
        // 监听之前的类对象
        Class classObj = object_getClass(self.kvo); 
        [self.kvo addObserver:self
                   forKeyPath:@"count"
                      options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                      context:countContext];
         // 监听之后的
        Class kvo_notify_class = object_getClass(self.kvo);
        // 强转一下类型
        struct cw_objc_class *newClass = (__bridge struct cw_objc_class *)kvo_notify_class;
        NSLog(@"KVOTest类对象: %@ - %p", classObj, classObj);
        NSLog(@"NSKVONotifying_KVOTest类对象: %@ - %p", kvo_notify_class, kvo_notify_class);
        NSLog(@"NSKVONotifying_KVOTest类对象的父类: %@ - %p", newClass->super_class, newClass->super_class);
    }
    

    控制台log如下:

    KVOTest类对象: KVOTest - 0x109ca8ab8
    NSKVONotifying_KVOTest类对象: NSKVONotifying_KVOTest - 0x600001edc090
    NSKVONotifying_KVOTest类对象的父类: KVOTest - 0x109ca8ab8
    

    NSKVONotifying_KVOTest类对象的父类地址和KVOTest类对象地址一致。

    补充说明:

    struct cw_objc_class {
       Class _Nonnull isa;
      Class _Nullable super_class;
    };
    
    struct cw_objc_class *newClass = (__bridge struct cw_objc_class *)kvo_notify_class
    

    Class类型的superclass指针是隐藏属性在外部不能访问,因为我们知道了Class其实也是结构体,并且前两个指针的类型:一个是isa,一个是superclass,所以这里我们可以自定义一个类似结构体,强转一下类型。这样我们就能拿到superclass了。

    4. _NSSetXXXValueAndNotify

    当属性发生改变时,我们需要通知到observer触发回调。之前的setter方法是满足不了需求的。所以底层继承原来的类,动态的创建了一个子类NSKVONotifying_XXX来重写setter方法,做了一些额外的操作。我们可以通过一段代码来验证:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.kvo = [[KVOTest alloc] init];
        self.kvo.count = 1;
        NSLog(@"监听之前的setter方法: %p", [self.kvo methodForSelector:@selector(setCount:)]);
        [self.kvo addObserver:self
                   forKeyPath:@"count"
                      options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                      context:countContext];
        NSLog(@"监听之后的setter方法: %p", [self.kvo methodForSelector:@selector(setCount:)]);
    }
    

    控制台log如下:

    OCTest[8254:266577] 监听之前的setter方法: 0x105e94f70
    OCTest[8254:266577] 监听之后的setter方法: 0x7fff2591f5cd
    

    监听前后的方法地址0x105e94f700x7fff2591f5cd不同,证实了setter方法的确被修改了。进一步断点调试:

    • 注册监听前的setter方法调用的是setCount毫无悬念。
    • 注册监听后的setter方法如上图,内部其实调用的是Foundation框架下的_NSSetLongLongValueAndNotify函数。

    关于_NSSetLongLongValueAndNotify函数,看字面意思就不难理解:setValue即设置新值,notify即通知触发observer回调。

    至于LongLong这是和属性的类型有关的。因为文中示例的countNSInteger类型,这里我们可以修改成NSString类型来验证下是否会发生变化.

    @interface KVOTest : NSObject
    @property (nonatomic, copy) NSString *count;
    @end
    

    断点调试:

    (lldb) po (IMP)0x7fff2591e98b
    (Foundation`_NSSetObjectValueAndNotify)
    

    改成NSString类型后,函数变成了_NSSetObjectValueAndNotify。有兴趣的话可以多改改试试,这里不再赘述。如果你懂逆向的话是可以找到Foundation框架下所有的这种方法。
    _NSSetValueAndNotify内部实现在接下来一节手动触发KVO会提到。

    5. 手动触发KVO

    我们给类KVOTest定义一个成员变量age

    @interface KVOTest : NSObject {
        @public NSInteger _age;
    }
    @end
    

    外部在ViewController中监听age, 点击屏幕的时候修改age的值

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.kvo = [[KVOTest alloc] init];
        self.kvo->_age = 1;
        [self.kvo addObserver:self
                   forKeyPath:@"age"
                      options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                      context:countContext];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        if (context == countContext)
        {
            NSInteger newCount = [[change valueForKey:NSKeyValueChangeNewKey] integerValue];
            NSInteger oldCount = [[change valueForKey:NSKeyValueChangeOldKey] integerValue];
            NSLog(@"new: %ld, old: %ld", newCount, oldCount);
        }
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.kvo->_age += 1;
    }
    

    定义成员变量是不会像@property关键字那样自动合成setter,getter方法的。这里的结果就是直接修改变量,并不能触发KVO机制。所以点击屏幕,回调是不会被触发的。直接修改变量值,不会触发KVO监听回调

    接下来我们手动创建一个setter方法试试:

    // .h文件
    - (void)setAge:(NSInteger)age;
    
    // .m文件
    - (void)setAge:(NSInteger)age {
        _age = age;
    }
    

    viewController中调用setter方法

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self.kvo setAge:18];
    }
    

    点击屏幕,监听回调触发


    手动实现setter方法就触发了回调(即使我们将setter方法里面的赋值代码注释掉),进一步验证了上文中提到的子类NSKVONotifying重写setter方法的验证。

    如果我们不按Apple的命名规范,自己定义一个setter方法会如何呢?

    // .h文件
    - (void)setNewAge:(NSInteger)age;
    
    // .m文件
    - (void)setNewAge:(NSInteger)age {
        _age = age;
    }
    

    viewController中改成自定义的方法:

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self.kvo setNewAge:18];
    }
    

    结果发现这样并不能触发KVO监听回调。

    接下来我们改造一下自定义setter方法,添加一对方法willChangeValueForKeydidChangeValueForKey

    - (void)setNewAge:(NSInteger)age {
        [self willChangeValueForKey:@"age"];
        _age = age;
        [self didChangeValueForKey:@"age"];
    }
    

    此时运行项目,再次点击屏幕修改值,KVO回调会被触发。而这也就是我们手动触发KVO的方式。

    6. 到这里我们可以总结一下KVO的流程:

    1. KVOTest类的instance对象注册属性监听后
    2. 系统通过runtime动态的创建了一个KVOTest的子类NSKVONotifying_KVOTest类,并将instance对象的isa指向它。
    3. 当属性发生改变时,调用NSKVONotifying_KVOTest的setter方法_NSSetXXXValueAndNotify
      内部实现:
      1. willChangeValueForKey
      2. 调用父类的setter赋值
      3. didChangeValueForKey
      4. 触发监听器observer的回调方法。

    7. 拓展补充

    我们可以写一个方法来获取NSKVONotifying_XXX中除了setter还有哪些方法

    - (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.kvo = [[KVOTest alloc] init];
        [self.kvo addObserver:self
                   forKeyPath:@"age"
                      options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                      context:countContext];
        [self printMethodnamesOfClass:object_getClass(self.kvo)];
    }
    

    控制台log

    NSKVONotifying_KVOTest: setAge:, class, dealloc, _isKVOA,
    
    1. 重写了class方法(这里返回的是父类KVOTest,这也是为什么做了监听操作之后调用[self.kvo class]和调用object_getClass(self.kvo)结果不同的原因),
    2. 重写了delloc方法(额外的做了一些操作,所以也需要一些额外释放销毁操作)
    3. 新增了一个_isKVOA (标识)

    相关文章

      网友评论

          本文标题:Objective-C KVO总结

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