【iOS】KVO的实现

2020-05-08  本文已影响0人  BeethOven

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监听的对象的区别

未使用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函数。伪代码实现

上一篇下一篇

猜你喜欢

热点阅读