手撕iOS底层26 -- 我理解的KVO

718 阅读5分钟

好久没写了, 就拿我没写过的KVO开刀吧~~

有工作的坑位的小伙伴,私信留言喔~~

面试好难哦

1. 什么是KVO?

官方文档

简介

KVO全称是Key-Value Observing , 通俗讲就是键值监听 , 可以用于监听某个对象属性值的改变,来回调观察者的observeValueForKeyPath: ofObject: change:context:方法。

2. KVO的基本使用

kvo-第 1 页.png

三部曲:

  • 注册KVO

    • observer: 观察者
    • keyPath:观察属性
    • options:枚举 观察newValue还是oldValue
    • context
      • 参数为空时使用NULL

    [self.person1 addObserver:self forKeyPath:@"age" options:options context:NULL]

  • dealloc移除KVO

    [self.person1 removeObserver:self forKeyPath:@"age"];

  • 书写KVO回调函数。

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        NSLog(@"监听到了%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    

3. KVO 存在的疑问

@interface Person : NSObject
@property(assign, nonatomic) int age;
@end
  
@implementation Person
@end
  
self.person1 = [[Person alloc] init];
self.person1.age = 1;

self.person2 = [[Person alloc] init];
self.person2.age = 2;
// 对person1对象添加观察者,观察属性age
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:NULL];

有如上代码,一样的类, 生成不同的对象,对person1对象age的改变,就可以被走到回调方法。 对peron2对象age的改变就不会。同样的类,同样的方法调用 ,这是为什么?所以这个需要我们去研究他们的不用。

4. KVO的原理分析

1.jpg

经过以上截图调试,发现被添加观察者的person1对象它的isa发生了改变。改变成了NSKVONotifying_Person这个类对象,就不再指向以前的Person类对象。

这个类是通过Runtime API生成的中间类,它继承我们原有的Person类,重写了被观察属性的setter方法。

从以上得出这个图

kvo-第 3 页.png

从本质看,像self.person1.age = 11; self.person2.age = 13;都是调用对象的setter方法。调用person1对象会发送通知,调用person2对象不会发送通知。

6.jpg

因为实例对象的方法存在它的类对象中,发送setAge方法,首先通过isa去找到它的类对象,查找此方法。在这里我们找到NSKVONotifying_Person类。找到了重写的方法。即调用中间类的setter方法。

person2对象调用的setter方法。因为它的isa还是Person类对象,所以调用还是Person类里的Setter方法。

通过官方文档 看得出, Automatic key-value observing的实现使用了isa-swizzling技术

2.jpg

⚠️ _NSSetIntValueAndNotify这个方法不是固定的_NSSet【XXXXXX】ValueAndNotify中间括起来的是会根据类型而改变。

比如把int age改成double age,相应的这个就会是 _NSSetDoubleValueAndNotify

4.1 KVO生成的中间类为什么重写class方法?

上面说到,新生成的类除了重写我们的setter方法,还有_isKVOA以及重写的deallocclass方法。

_isKVOA方法返回true标明是KVOdeallco方法作一些清理工作。那class为什么还要重写

4.jpg

通过Runtime底层API获取的类对象是isa真正指向的类对象

9.jpg

和使用OC方法获取的类对象是中间类重写的class方法的返回值

7.jpg

  1. 从开发角度来看。我们使用Person类生成实例对象, 我们认知的类型就是Person, 虽然系统中间生成了中间类,它的isa指向改变了,但是苹果并不想把这个中间类直接暴露出来。所以中间类重写了class方法, 直接返回了Person类。 好处就是屏蔽了内部实现,隐藏这个类的实现,在OC代码层面保持一致性。
  2. 如果子类不重写这个方法,调用class方法,首先通过isa指向去类对象NSKVONotifying_Person查找这个方法,没有找到就一层一层通过superclass查找,直到NSObject类, 默认实现就是直接返回return object_getClass(self), 相当于直接返回NSKVONotifying_Person这个类对象。
  3. 官方文档可以得知, isa指针是维护调度表的对象类,该表包含指向实现的方法指针,以及其它。当被观察者属性被观察者对象观察时,被观察对象的isa就改变了,因此,isa不一定反映实例对象真实的实际类。告诉我们不要依赖isa指针来确定类型,相反,使用class方法来确定对象实际类型。
// 遍历打印类方法
- (void)printMethodNameOfClass:(Class)cls {
    unsigned int count = 0;
    NSMutableString *methodNames = [NSMutableString string];
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        [methodNames appendString:NSStringFromSelector(method_getName(method))];
        [methodNames appendString:@"\n"];
    }
    free(methodList);
    NSLog(@"%@", methodNames);
}
// 这里也可以这么调用, ⚠️ 必须在注册完KVO后调用个,否则报错, 因为这个类只有在注册KVO后才会动态生成。 
// [self printMethodNameOfClass:NSClassFromString(@"NSKVONotifying_Person")];

5.jpg

- (void)printSuperClassChainOfClass:(Class)cls {
    // 注册类的总个数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组,其中包含给定的对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class *classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i < count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

10.jpg

4.2 调用顺序分析

17.jpg

大概模拟了下中间类的运作NSKVONotifying_Person,子类先去调用Foundation框架的方法,在这个方法里先调用willChangeValueForKey方法,在去调用父类的实现, 最后调用didChangeValueForKey去通知Observer

18.jpg

4.3 移除观察者后发生什么?

11.jpg

移除观察者后isa指回原来的类,并且动态子类不会销毁,通过打印它还是存在的。

5. Foundation窥探

通过调用顺序分析知道, KVO是在重写的setter方法调用了_NSSetIntValueAndNotify这个方法, 而这个方法属于Foundation框架中, 无法看到其源码实现,那我们从另一种角度来看下Foundation内部的实现

12.jpg

通过image list调试查看Foundation位置,找到它通过Hopper反汇编工具来查看

13.jpg

删除Int后搜索_NSSetValueAndNotify,出来好多各种类型的方法名字, 也就证明了,KVO观察的各种类型属性会根据属性的类型不同调用不同的Foundation方法。

14.jpg

通过终端命令nm加过滤, 搜索查看这些方法名字。

15.jpg

6. 手动关闭&手动发送KVO

当我们在一个类中,想关闭某个属性的KVO或者全部关闭(只有某个属性可以使用KVO)

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
// 对某个key禁用
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"nick"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
// 简便写法 直接对nick属性禁止
+ (BOOL)automaticallyNotifiesObserversOfNick {
    return YES;
}
  • 使用automaticallyNotifiesObserversForKey返回NO,对象的全部属性都不可以KVO

  • 简便写法,是根据对象属性的重写方法automaticallyNotifiesObserversOf<Key>形式

  • 如果全部属性禁止KVO了, 但又想单独对其中一个属性使用,可以手动触发

    - (void)setNick:(NSString *)nick{
        [self willChangeValueForKey:@"nick"];
        _nick = nick;
        [self didChangeValueForKey:@"nick"];
    }
    
    • 使用成对willChangeValueForKeydidChangeValueForKey手动触发,⚠️如果只调用didChangeValueForKey也是不会触发,因为在didChangeValueForKey会判断willChangeValueForKey;

7. 可变数组如何触发KVO

Person中又一个可变数组

@interface LGPerson : NSObject
  @property (nonatomic, strong) NSMutableArray *dateArray;
@end
      [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];

  self.person = dateArray = [NSMutableArray arrayWithCapacity:1];
// 这样会触发KVO吗????
[self.person.dateArray addObject:@1000];

显然通过[self.person.dateArray addObject:@1000];这行代码不会触发KVO,因为KVO本质是要去调用setter方法。 这里显然并没有。

[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@22];

8.jpg

LAST - 面试题

1. iOS用什么方式实现对一个对象的KVO? (KVO本质是什么)?
  • 利用RuntimeAPI动态生成子类,并且让实例对象isa指向全新生成的子类 (也就是isa-swizzling技术)
  • 修改属性会调用 通过重写属性的setter方法调用Foundation_NSSetXXXValueNotify函数、、
    • willChangeValueForKey
    • 父类原来的实现
    • didChangeValueForKey 内部会触发监听器observerValueForKeyPath:ofObject:chang:context方法
  • 动态子类重写了观察属性setter 方法 class方法, dealloc方法 以及新增isKVOA方法
2. 如何手动处触发KVO?

使用成对willChangeValueForKeydidChangeValueForKey手动触发。单独使用didChangeValueForKey也无法触发,必须成对包裹。

3. 直接修改成员变量会处罚KVO吗?

不会,如self.person->age = 10; 根据本质来看,需要调用setter方法

4. 通过kvc调用会触发kvo吗

会触发KVO, 如[self.person setValue:@"zhangsna" forKey:@"name"]

它会触发setter方法。所以会触发KVO回调。


欢迎大佬留言指正😄,码字不易,觉得好给个赞👍 有任何表达或者理解失误请留言交流;共同进步;