InterView-OC基础-3.KVO

347 阅读13分钟

0.复习流程

  • 先回答键值监听的概念
  • 说一下简单使用

2. 回答出通过isa推出的派生类的概念:

  • 借助runtime的 object_getClass()传入对象返回其类对象,传入类对象,返回其元类对象,也就是相当于isa的作用,分别打印其被监听前后
self.p1 = [[Person alloc] init];
    
    NSLog(@"监听前----%@",object_getClass(self.p1));
    [self.p1 addObserver:self
              forKeyPath:@"age"
                 options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
    NSLog(@"监听后----%@",object_getClass(self.p1));
    
    self.p2 = [[Person alloc] init];

打印结果为

监听前----Person
监听后----NSKVONotifying_Person
  • 断点调试,在监听前 和 监听后分别在控制台p 对象.isa,派生类产生回答出runtime
监听前
 p self.person.isa
(Class) $5 = Person
监听后
 p self.person.isa
(Class) $6 = NSKVONotifying_Person

3. 回答出重写setter方法

通过 - (IMP)methodForSelector:(SEL)aSelector方法来获取方法的实现,以此来查看,添加监听前后,setter方法实现是否进行了更改,代码如下

self.p2 = [[Person alloc] init];
    IMP imp1 =  [self.p2 methodForSelector:@selector(setAge:)];
    [self.p2 addObserver:self
              forKeyPath:@"age"
                 options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
    IMP imp2 =  [self.p2 methodForSelector:@selector(setAge:)];

打印结果如下 image.png 可以看出,被监听以后,setter方法实现变为了_NSSetIntValueAndNotify,这方法可以改为 _NSSet*AndNotify() 这个 * 的意思根据监听属性的不同,实现也不一样,具体有

 _NSSetBoolValueAndNotify 
 _NSSetCharValueAndNotify 
 _NSSetDoubleValueAndNotify 
 _NSSetFloatValueAndNotify 
 _NSSetIntValueAndNotify 
 _NSSetLongLongValueAndNotify 
 _NSSetLongValueAndNotify 
 _NSSetObjectValueAndNotify 
 _NSSetPointValueAndNotify 
 _NSSetRangeValueAndNotify 
 _NSSetRectValueAndNotify 
 _NSSetShortValueAndNotify 
 _NSSetSizeValueAndNotify 
 _NSSetUnsignedCharValueAndNotify 
 _NSSetUnsignedIntValueAndNotify 
 _NSSetUnsignedLongLongValueAndNotify 
 _NSSetUnsignedLongValueAndNotify 
 _NSSetUnsignedShortValueAndNotify 
  • 这个c函数有三点作用,并且调用顺序如下
  [self willChangeValueForKey:@"属性名"];
  setter
  [self didChangeValueForKey:@"属性名"];

我们通过重写这个类的以上三个方法,并打印,可以看出调用过程

- (void)setAge:(int)age {
    _age = age;
    NSLog(@"setAge");
}

- (void)willChangeValueForKey:(NSString *)key {
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"didChangeValueForKey -- begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey -- end");
}

我们在给person的age进行赋值以后,控制台打印结果为

willChangeValueForKey
setAge
didChangeValueForKey -- begin
didChangeValueForKey -- end

4. 对class 方法,dealloc ,_isKVOA的重写

首先通过运行时来验证,在被监听以后的这个对象的类对象的方法列表,开看出以上方法被重写了,如果没有重写,以上方法不应该出现在Person的类结构中,而应该出现父类或者基类的方法里列表中

 self.p3 = [[Person alloc] init];
    [self printMethodNamesOfClass:object_getClass(self.p3)];
    [self.p3 addObserver:self
              forKeyPath:@"age"
                 options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
    
    [self printMethodNamesOfClass:object_getClass(self.p3)];

打印结果为

Person setAge:, age,
NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,

明显可以看出增加了三个方法,其中printMethodNamesOfClass是打印一个类的方法列表,实现源码为

- (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);
}

1. class

  • 证明方式:就是通过打印person的class方法调用返回值object_getClass(person对象)来查看,在正常情况下,二者都应该返回person对象的类对象,也就是Person,但是我们在前面章节可以看出,object_getClass返回的值是NSKVONotifying_Person ,但是调用class返回的* 依旧是Person类,可以看出对象被监听以后,class方法被重写了 目的:为了屏蔽代码的实现逻辑,隐藏了NSKVONotifying_Person 的派生类,避免对KVO实际逻辑的探索

5.手动触发KVO

手动调用以下两行代码即可

  [self willChangeValueForKey:@"属性名"];
  setter
  [self didChangeValueForKey:@"属性名"];

6.直接修改成员变量不会触发KVO

因为没有涉及到触发setter方法

7.常见面试题

1.当我们使用 KVO 监听数组或集合类型的属性时会发生什么?有哪些注意事项?

在使用 KVO(Key-Value Observing)监听数组或集合类型的属性时,会有一些特定的行为和注意事项。下面是一些关键点:

1. KVO 工作机制

KVO 是通过监听对象的属性变化来触发回调的。当属性的值发生变化时,KVO 会通知观察者。例如,如果一个对象的数组属性发生了变化(如添加或删除元素),KVO 会触发观察者的回调。

2. 数组或集合类型的特性

对于数组或集合(如 NSArray, NSMutableArray, NSSet, NSMutableSet),KVO 主要监听的是对象本身的属性变化。具体来说,数组的变化(如元素的增加、删除或重新排序)会通过 willChangedidChange 来进行通知。

  • 对于可变集合(如 NSMutableArrayNSMutableSet),在修改集合内容时,KVO 会触发 willChangedidChange 通知,表示集合将发生变化。
  • 对于不可变集合(如 NSArrayNSSet),一旦对象的内容改变,KVO 也会发出通知。

3. KVO 无法监听元素内部变化

如果你对数组或集合内的元素进行修改(如修改元素的属性),KVO 不会通知观察者。也就是说,KVO 只监听集合对象的引用变化(如集合本身的增加或删除元素),而不会监听元素的内部变化。

如果需要监听元素属性的变化,必须对每个元素进行单独的 KVO 监听。

4. 需要确保线程安全

在修改数组或集合的内容时(例如在多线程环境下),需要特别注意线程安全问题。直接修改集合内容可能会引发崩溃或其他未定义行为。因此,应确保在修改集合时对其进行适当的同步。

5. 需要手动观察

对于数组或集合的 KVO 监听,通常需要手动添加和移除观察者。在不再需要监听时,记得调用 removeObserver 移除观察者,以避免内存泄漏或崩溃。

6. 通知传递的粒度

KVO 仅在整个集合的引用发生变化时通知观察者。换句话说,某个元素被删除或替换时,KVO 会通知你集合的变化,但如果集合中的一个元素被修改(例如它的属性被改变),KVO 不会通知你。

7. 一些常见的变更

  • 对可变数组或集合的元素进行 addObjectremoveObject 操作时,KVO 会通知观察者。
  • 对集合本身重新赋值或改变引用时,KVO 也会生效。

8. 例外情况:NSMutableArray 的 KVO 通知

当你使用 NSMutableArray 时,如果添加、删除或替换元素时,KVO 会触发通知。通常,如果你想监听数组的变化,你可以监听数组的 count 属性或者通过观察 mutableArrayValueForKey: 进行监听。

总结

  • KVO 能监听数组或集合对象的引用变化(如增加、删除或替换元素),但无法监听集合内元素的变化。
  • 在多线程环境下使用 KVO 时,要注意线程安全。
  • 如果数组或集合元素的内部状态发生变化,需为每个元素单独添加 KVO 观察。

2/2

4o mini

您说:
  1. 现代 KVO API 与旧版 API 有什么不同?为什么推荐使用现代 API?
ChatGPT 说:

ChatGPT

现代 KVO(Key-Value Observing)API 与旧版 KVO API 主要有以下几个关键的区别。随着 iOS 和 macOS 平台的发展,Apple 引入了新的 KVO API,旨在提高性能、减少错误并简化代码的管理。以下是两者的主要区别和现代 API 的推荐原因:

1. 旧版 KVO API

在旧版 KVO API 中,观察者通常通过以下方式注册和注销:

objc
复制代码
[object addObserver:observer forKeyPath:keyPath options:options context:context];
[object removeObserver:observer forKeyPath:keyPath];
  • addObserver:forKeyPath: 方法用于注册观察者,观察对象的指定键路径。
  • removeObserver:forKeyPath: 方法用于注销观察者。

旧版 API 中,所有 KVO 观察者的管理都通过手动管理 addObserverremoveObserver 方法调用。如果忘记调用 removeObserver,则会导致内存泄漏或崩溃。手动管理观察者的生命周期也容易出错。

注意:

  • KVO 的实现底层是通过消息转发和动态方法解析实现的,因此会影响性能。
  • 由于没有现代 API 中的优化,一旦发生错误(如尝试访问已移除的观察者),程序容易崩溃。

2. 现代 KVO API

Apple 在 iOS 10 和 macOS 10.12 中引入了现代 KVO API,使用 NSKeyValueObservation 类来简化和安全地管理 KVO 观察者。现代 API 改进了管理观察者生命周期的方式,不需要手动调用 removeObserver

使用方式:

objc
复制代码
@objc var observedObject = ObservableObject()

var observation: NSKeyValueObservation?

func startObserving() {
    observation = observedObject.observe(.someProperty, options: [.new, .old]) { object, change in
        // Handle property change
    }
}

func stopObserving() {
    observation = nil // Automatically removes observer
}
  • 简化代码:现代 API 引入了 NSKeyValueObservation 类型的对象来自动管理观察者的生命周期,不再需要显式地调用 removeObserver
  • 自动管理生命周期:当 NSKeyValueObservation 对象被销毁时,它会自动取消观察。无需担心忘记调用 removeObserver 导致的内存泄漏。
  • 强类型化:现代 API 是强类型的,采用键路径(Key Path)的形式,可以直接观察对象属性,而不像旧版 API 那样使用字符串形式的键路径,减少了出错的风险。
  • 异步回调和线程安全:现代 API 更好地支持异步回调,使得它在多线程环境下更为安全。

3. 现代 API 的优势

推荐使用现代 KVO API 的原因

  • 自动管理观察者生命周期:无需手动移除观察者,避免了内存泄漏或崩溃的风险。
  • 简洁的代码和更好的可读性:现代 API 使用更简洁的语法,支持键路径表达式,减少了错误的发生。
  • 线程安全性:现代 API 提供更强的线程安全保证,适用于多线程环境。
  • 易于调试:通过 NSKeyValueObservation,可以轻松调试 KVO 的行为,避免传统的 KVO API 调用错误。

4. 比较

  • 旧版 API:通过 addObserverremoveObserver 手动管理观察者,需要小心处理观察者的添加和移除。缺乏强类型检查,易发生拼写错误和其他问题。
  • 现代 API:使用 NSKeyValueObservation 类来自动管理生命周期。API 更加简洁、安全,支持键路径表达式,能更方便地处理异步和线程安全问题。

总结

现代 KVO API 更加安全、简洁、易于使用,并且自动处理观察者的生命周期,因此推荐使用现代 API。它减少了手动管理和潜在错误的复杂性,提高了代码的可维护性和健壮性。

4o mini

您说:
  1. 如何解决 KVO 中可能发生的循环引用问题?
ChatGPT 说:

ChatGPT

在使用 KVO(Key-Value Observing)时,循环引用(retain cycle)是一个常见的问题。循环引用发生在观察者和被观察对象之间相互持有对方时,导致内存泄漏,因为它们不能被释放。解决 KVO 中的循环引用问题通常有以下几种方法。

1. 使用 weakunowned 引用观察者

在添加 KVO 观察者时,最常见的循环引用问题是观察者(通常是视图控制器或其他对象)被 KVO 观察的对象强引用。为了避免这种情况,我们应该确保观察者对象不会被强引用,可以通过 weakunowned 修饰符来避免循环引用。

示例:使用 weak 引用

objc
复制代码
class MyClass {
    var observedObject: ObservableObject
    var observation: NSKeyValueObservation?

    init(observedObject: ObservableObject) {
        self.observedObject = observedObject
        // 使用 weak 防止循环引用
        observation = observedObject.observe(.someProperty, options: [.new, .old]) { [weak self] object, change in
            guard let self = self else { return }
            // 处理属性变化
        }
    }
}

在这个示例中,[weak self] 确保 self(通常是视图控制器或其他对象)在闭包中是弱引用的,这样如果 self 被销毁,KVO 观察者不会持有它,避免了循环引用。

示例:使用 unowned 引用

如果你确信观察者在 KVO 的生命周期内始终存在,并且观察者不会被提前释放,可以使用 unowned selfunowned 引用与 weak 类似,但不同的是,如果 self 被销毁,unowned self 会导致崩溃,而 weak 引用则会变为 nil

objc
复制代码
observation = observedObject.observe(.someProperty, options: [.new, .old]) { [unowned self] object, change in
    // 处理属性变化
}

使用 unowned 可以减少某些情况下的内存开销,特别是当你确定对象生命周期的一致性时。

2. 移除观察者

在旧版 KVO API 中,循环引用的常见原因是未正确移除观察者。如果你在使用旧版 KVO API(如 addObserver:forKeyPath:)时忘记移除观察者,观察者将会持有被观察对象,从而导致循环引用。因此,确保在适当的时候(例如对象销毁时)手动移除观察者是至关重要的。

objc
复制代码
// 在对象销毁时移除观察者
deinit {
    observedObject.removeObserver(self, forKeyPath: "someProperty")
}

在现代 KVO API 中(使用 NSKeyValueObservation),观察者的生命周期由 NSKeyValueObservation 自动管理,无需手动移除,但仍然要注意正确释放引用,避免手动管理引发的循环引用。

3. 避免在闭包中强引用自身

当使用闭包(block)作为观察者时,尤其是在处理 KVO 时,需要小心避免闭包内部强引用自身。比如,闭包内如果直接使用 self,可能会导致循环引用。使用 weakunowned 引用来避免闭包强引用外部对象。

objc
复制代码
observation = observedObject.observe(.someProperty, options: [.new, .old]) { [weak self] object, change in
    guard let self = self else { return }
    // 处理变化
}

这里的 weak self 确保如果 self 被销毁,闭包不会继续强引用它,避免了内存泄漏。

4. 使用 NSKeyValueObservation 的生命周期管理

现代 KVO API(NSKeyValueObservation)会自动管理观察者的生命周期。当 NSKeyValueObservation 对象被销毁时,KVO 观察者会自动从观察列表中移除,从而避免了传统 KVO API 中的循环引用问题。

你可以通过确保 NSKeyValueObservation 对象的生命周期结束来避免循环引用:

objc
复制代码
var observation: NSKeyValueObservation?

func observeObject() {
    observation = observedObject.observe(.someProperty, options: [.new, .old]) { [weak self] object, change in
        // 处理属性变化
    }
}

// 当观察结束时,observation 将被销毁,自动移除观察者

5. 清理和解除引用

确保在适当的地方清理引用,尤其是在观察结束或对象销毁时。对于 KVO 观察者,应该确保移除任何不再需要的引用,以避免不必要的内存占用。

总结

  • 使用 weakunowned 引用:确保观察者对象不会被强引用,避免循环引用。
  • 正确移除观察者:如果使用旧版 KVO API,确保在不再需要观察时移除观察者。
  • 避免在闭包中强引用 self:使用 weakunowned 引用避免闭包导致的循环引用。
  • 利用现代 KVO API(NSKeyValueObservation)自动管理观察者的生命周期:避免手动管理观察者,减少内存泄漏的风险。

这些方法可以有效地避免 KVO 中的循环引用问题,保证内存的正确管理和程序的稳定运行。

###7.kvc设置值可以触发KVO 因为KVC赋值的方法 setValueforKey 底层会调用setter方法,所以能触发 image.png image.png