0.复习流程
- 先回答键值监听的概念
- 说一下简单使用
2. 回答出通过isa推出的派生类的概念:
- 借助runtime的
object_getClass()
传入对象返回其类对象,传入类对象,返回其元类对象,也就是相当于isa的作用,分别打印其被监听前后
swift 代码解读复制代码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];
打印结果为
css 代码解读复制代码监听前----Person
监听后----NSKVONotifying_Person
- 断点调试,在监听前 和 监听后分别在控制台
p 对象.isa
,派生类产生回答出runtime
swift 代码解读复制代码监听前
p self.person.isa
(Class) $5 = Person
监听后
p self.person.isa
(Class) $6 = NSKVONotifying_Person
3. 回答出重写setter方法
通过 - (IMP)methodForSelector:(SEL)aSelector
方法来获取方法的实现,以此来查看,添加监听前后,setter
方法实现是否进行了更改,代码如下
swift 代码解读复制代码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:)];
打印结果如下
可以看出,被监听以后,setter方法实现变为了
_NSSetIntValueAndNotify
,这方法可以改为 _NSSet*AndNotify()
这个 * 的意思根据监听属性的不同,实现也不一样,具体有
代码解读复制代码_NSSetBoolValueAndNotify _NSSetCharValueAndNotify _NSSetDoubleValueAndNotify _NSSetFloatValueAndNotify _NSSetIntValueAndNotify _NSSetLongLongValueAndNotify _NSSetLongValueAndNotify _NSSetObjectValueAndNotify _NSSetPointValueAndNotify _NSSetRangeValueAndNotify _NSSetRectValueAndNotify _NSSetShortValueAndNotify _NSSetSizeValueAndNotify _NSSetUnsignedCharValueAndNotify _NSSetUnsignedIntValueAndNotify _NSSetUnsignedLongLongValueAndNotify _NSSetUnsignedLongValueAndNotify _NSSetUnsignedShortValueAndNotify
- 这个c函数有三点作用,并且调用顺序如下
objectivec 代码解读复制代码 [self willChangeValueForKey:@"属性名"];
setter
[self didChangeValueForKey:@"属性名"];
我们通过重写这个类的以上三个方法,并打印,可以看出调用过程
objectivec 代码解读复制代码- (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进行赋值以后,控制台打印结果为
arduino代码解读复制代码willChangeValueForKey setAge didChangeValueForKey -- begin didChangeValueForKey -- end
4. 对class 方法,dealloc ,_isKVOA的重写
首先通过运行时来验证,在被监听以后的这个对象的类对象的方法列表,开看出以上方法被重写了,如果没有重写,以上方法不应该出现在Person的类结构中,而应该出现父类或者基类的方法里列表中
swift 代码解读复制代码 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)];
打印结果为
objectivec 代码解读复制代码Person setAge:, age,
NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,
明显可以看出增加了三个方法,其中printMethodNamesOfClass
是打印一个类的方法列表,实现源码为
scss 代码解读复制代码- (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
手动调用以下两行代码即可
objectivec 代码解读复制代码 [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 主要监听的是对象本身的属性变化。具体来说,数组的变化(如元素的增加、删除或重新排序)会通过 willChange
和 didChange
来进行通知。
- 对于可变集合(如
NSMutableArray
或NSMutableSet
),在修改集合内容时,KVO 会触发willChange
和didChange
通知,表示集合将发生变化。 - 对于不可变集合(如
NSArray
或NSSet
),一旦对象的内容改变,KVO 也会发出通知。
3. KVO 无法监听元素内部变化
如果你对数组或集合内的元素进行修改(如修改元素的属性),KVO 不会通知观察者。也就是说,KVO 只监听集合对象的引用变化(如集合本身的增加或删除元素),而不会监听元素的内部变化。
如果需要监听元素属性的变化,必须对每个元素进行单独的 KVO 监听。
4. 需要确保线程安全
在修改数组或集合的内容时(例如在多线程环境下),需要特别注意线程安全问题。直接修改集合内容可能会引发崩溃或其他未定义行为。因此,应确保在修改集合时对其进行适当的同步。
5. 需要手动观察
对于数组或集合的 KVO 监听,通常需要手动添加和移除观察者。在不再需要监听时,记得调用 removeObserver
移除观察者,以避免内存泄漏或崩溃。
6. 通知传递的粒度
KVO 仅在整个集合的引用发生变化时通知观察者。换句话说,某个元素被删除或替换时,KVO 会通知你集合的变化,但如果集合中的一个元素被修改(例如它的属性被改变),KVO 不会通知你。
7. 一些常见的变更
- 对可变数组或集合的元素进行
addObject
或removeObject
操作时,KVO 会通知观察者。 - 对集合本身重新赋值或改变引用时,KVO 也会生效。
8. 例外情况:NSMutableArray 的 KVO 通知
当你使用 NSMutableArray
时,如果添加、删除或替换元素时,KVO 会触发通知。通常,如果你想监听数组的变化,你可以监听数组的 count
属性或者通过观察 mutableArrayValueForKey:
进行监听。
总结
- KVO 能监听数组或集合对象的引用变化(如增加、删除或替换元素),但无法监听集合内元素的变化。
- 在多线程环境下使用 KVO 时,要注意线程安全。
- 如果数组或集合元素的内部状态发生变化,需为每个元素单独添加 KVO 观察。
2/2
4o mini
您说:
- 现代 KVO API 与旧版 API 有什么不同?为什么推荐使用现代 API?
ChatGPT 说:
ChatGPT
现代 KVO(Key-Value Observing)API 与旧版 KVO API 主要有以下几个关键的区别。随着 iOS 和 macOS 平台的发展,Apple 引入了新的 KVO API,旨在提高性能、减少错误并简化代码的管理。以下是两者的主要区别和现代 API 的推荐原因:
1. 旧版 KVO API
在旧版 KVO API 中,观察者通常通过以下方式注册和注销:
ini 代码解读复制代码objc
复制代码
[object addObserver:observer forKeyPath:keyPath options:options context:context];
[object removeObserver:observer forKeyPath:keyPath];
addObserver:forKeyPath:
方法用于注册观察者,观察对象的指定键路径。removeObserver:forKeyPath:
方法用于注销观察者。
旧版 API 中,所有 KVO 观察者的管理都通过手动管理 addObserver
和 removeObserver
方法调用。如果忘记调用 removeObserver
,则会导致内存泄漏或崩溃。手动管理观察者的生命周期也容易出错。
注意:
KVO
的实现底层是通过消息转发和动态方法解析实现的,因此会影响性能。- 由于没有现代 API 中的优化,一旦发生错误(如尝试访问已移除的观察者),程序容易崩溃。
2. 现代 KVO API
Apple 在 iOS 10 和 macOS 10.12 中引入了现代 KVO API,使用 NSKeyValueObservation
类来简化和安全地管理 KVO 观察者。现代 API 改进了管理观察者生命周期的方式,不需要手动调用 removeObserver
。
使用方式:
typescript 代码解读复制代码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:通过
addObserver
和removeObserver
手动管理观察者,需要小心处理观察者的添加和移除。缺乏强类型检查,易发生拼写错误和其他问题。 - 现代 API:使用
NSKeyValueObservation
类来自动管理生命周期。API 更加简洁、安全,支持键路径表达式,能更方便地处理异步和线程安全问题。
总结
现代 KVO API 更加安全、简洁、易于使用,并且自动处理观察者的生命周期,因此推荐使用现代 API。它减少了手动管理和潜在错误的复杂性,提高了代码的可维护性和健壮性。
4o mini
您说:
- 如何解决 KVO 中可能发生的循环引用问题?
ChatGPT 说:
ChatGPT
在使用 KVO(Key-Value Observing)时,循环引用(retain cycle)是一个常见的问题。循环引用发生在观察者和被观察对象之间相互持有对方时,导致内存泄漏,因为它们不能被释放。解决 KVO 中的循环引用问题通常有以下几种方法。
1. 使用 weak
或 unowned
引用观察者
在添加 KVO 观察者时,最常见的循环引用问题是观察者(通常是视图控制器或其他对象)被 KVO 观察的对象强引用。为了避免这种情况,我们应该确保观察者对象不会被强引用,可以通过 weak
或 unowned
修饰符来避免循环引用。
示例:使用 weak
引用
swift 代码解读复制代码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 self
。unowned
引用与 weak
类似,但不同的是,如果 self
被销毁,unowned self
会导致崩溃,而 weak
引用则会变为 nil
。
php 代码解读复制代码objc
复制代码
observation = observedObject.observe(.someProperty, options: [.new, .old]) { [unowned self] object, change in
// 处理属性变化
}
使用 unowned
可以减少某些情况下的内存开销,特别是当你确定对象生命周期的一致性时。
2. 移除观察者
在旧版 KVO API 中,循环引用的常见原因是未正确移除观察者。如果你在使用旧版 KVO API(如 addObserver:forKeyPath:
)时忘记移除观察者,观察者将会持有被观察对象,从而导致循环引用。因此,确保在适当的时候(例如对象销毁时)手动移除观察者是至关重要的。
php 代码解读复制代码objc
复制代码
// 在对象销毁时移除观察者
deinit {
observedObject.removeObserver(self, forKeyPath: "someProperty")
}
在现代 KVO API 中(使用 NSKeyValueObservation
),观察者的生命周期由 NSKeyValueObservation
自动管理,无需手动移除,但仍然要注意正确释放引用,避免手动管理引发的循环引用。
3. 避免在闭包中强引用自身
当使用闭包(block)作为观察者时,尤其是在处理 KVO 时,需要小心避免闭包内部强引用自身。比如,闭包内如果直接使用 self
,可能会导致循环引用。使用 weak
或 unowned
引用来避免闭包强引用外部对象。
php 代码解读复制代码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
对象的生命周期结束来避免循环引用:
typescript 代码解读复制代码objc
复制代码
var observation: NSKeyValueObservation?
func observeObject() {
observation = observedObject.observe(.someProperty, options: [.new, .old]) { [weak self] object, change in
// 处理属性变化
}
}
// 当观察结束时,observation 将被销毁,自动移除观察者
5. 清理和解除引用
确保在适当的地方清理引用,尤其是在观察结束或对象销毁时。对于 KVO 观察者,应该确保移除任何不再需要的引用,以避免不必要的内存占用。
总结
- 使用
weak
或unowned
引用:确保观察者对象不会被强引用,避免循环引用。 - 正确移除观察者:如果使用旧版 KVO API,确保在不再需要观察时移除观察者。
- 避免在闭包中强引用
self
:使用weak
或unowned
引用避免闭包导致的循环引用。 - 利用现代 KVO API(
NSKeyValueObservation
)自动管理观察者的生命周期:避免手动管理观察者,减少内存泄漏的风险。
这些方法可以有效地避免 KVO 中的循环引用问题,保证内存的正确管理和程序的稳定运行。
###7.kvc设置值可以触发KVO 因为KVC赋值的方法 setValueforKey 底层会调用setter方法,所以能触发
![]()
评论记录:
回复评论: