iOS中KVC与KVO工作原理及面试题解析
一、KVC(Key-Value Coding)工作原理
核心机制:通过字符串键动态访问对象属性,基于NSKeyValueCoding协议实现。
1. 取值流程(valueForKey:)
按顺序查找getKey/key/isKey方法 → 检查accessInstanceVariablesDirectly是否允许 → 按_key/_isKey/key/iskey顺序访问成员变量 → 调用valueForUndefinedKey:(默认抛出异常)。
示例:若属性为age,优先调用getAge或age方法,若无则查找_age成员变量。
2. 赋值流程(setValue:forKey:)
按顺序查找setKey:/_setKey:方法 → 检查accessInstanceVariablesDirectly → 按_key/_isKey/key/iskey顺序赋值成员变量 → 调用setValue:forUndefinedKey:(默认抛出异常)。
示例:调用setValue:@10 forKey:@"age"时,优先调用setAge:方法,若无则直接修改_age变量。
3. 特殊场景处理
集合属性:若属性为NSArray,KVC会查找countOfKey、objectInKeyAtIndex等方法,返回代理集合NSKeyValueArray。
私有变量:KVC可直接访问以_开头的私有成员变量,但需accessInstanceVariablesDirectly返回YES(默认允许)。
4. 安全使用建议
避免使用基本数据类型(如int),改用NSNumber,防止后台返回null导致崩溃。
重写valueForUndefinedKey:和setValue:forUndefinedKey:处理未定义属性,避免程序崩溃。
二、KVO(Key-Value Observing)工作原理
核心机制:基于KVC和动态派发技术,通过isa-swizzling生成子类实现。
1. 底层实现
动态子类:当对象被观察时,系统生成NSKVONotifying_OriginalClass子类,重写被观察属性的setter方法。
isa指针修改:将原对象的isa指针指向动态子类,使setter调用重定向至子类实现。
通知触发:子类setter中调用willChangeValueForKey:和didChangeValueForKey:,最终触发观察者的observeValueForKeyPath:ofObject:change:context:回调。
2. 触发条件
必须通过KVC或setter方法修改属性值,直接修改成员变量不会触发KVO。
集合属性:需使用mutableArrayValueForKey:等方法获取代理对象,通过代理对象操作集合才能触发通知。
3. 手动触发KVO
直接调用willChangeValueForKey:和didChangeValueForKey:,适用于属性值依赖其他计算的场景。
- (void)setCustomValue:(id)value {
[self willChangeValueForKey:@"customValue"];
_customValue = value;
[self didChangeValueForKey:@"customValue"];
}
4. Swift中的限制
属性需声明为@objc dynamic,且类必须继承自NSObject。
观察者需显式注册,且keyPath为字符串,可能引发编译时错误。
三、高阶面试题及答案
1. KVO的本质是什么?如何实现动态子类?
答案:KVO通过isa-swizzling生成NSKVONotifying子类,重写setter方法并插入通知逻辑。动态子类的isa指针指向该子类,使setter调用被拦截。
细节:子类重写class方法返回原类名,欺骗外部调用者;重写dealloc释放资源。
2. 直接修改成员变量会触发KVO吗?为什么?
答案:不会。因为KVO依赖setter方法或KVC操作,直接修改成员变量不会调用willChangeValueForKey:和didChangeValueForKey:。
3. 如何在Swift中正确使用KVO?
答案:
属性需@objc dynamic修饰,类继承自NSObject。
显式注册观察者:addObserver(self, forKeyPath: "property", options: .new, context: nil)。
实现observeValue(forKeyPath:of:change:context:)回调。
4. KVO如何处理集合属性的变化?
答案:使用mutableArrayValueForKey:获取代理对象,通过该对象操作集合(如addObject:),系统会自动触发通知。直接修改数组内容(如array[0] = newValue)不会触发KVO。
5. KVO的context参数有什么作用?
答案:用于区分不同的观察者或属性。在注册时传入唯一值(如静态变量地址),回调中通过context判断触发来源,避免继承或多监听时的混淆。
6. KVC赋值时,系统如何查找属性?
答案:按顺序查找setKey: → _setKey: → 检查accessInstanceVariablesDirectly → 查找_key/_isKey/key/iskey成员变量。若全部失败,调用setValue:forUndefinedKey:。
7. KVO的性能问题如何优化?
答案:
减少不必要的观察,及时调用removeObserver:。
使用Combine框架替代KVO,避免动态子类和isa-swizzling的开销。
合并多次属性变化,通过字典缓存最新值,批量处理通知。
8. KVC的安全使用需要注意什么?
答案:
避免使用基本数据类型,改用NSNumber防止null崩溃。
重写setValue:forUndefinedKey:和valueForUndefinedKey:处理未定义属性。
谨慎处理accessInstanceVariablesDirectly,避免意外访问私有变量。
四、总结
KVC和KVO是iOS开发中的核心机制,需深入理解其底层实现以应对高阶问题。面试中需关注动态子类、isa-swizzling、集合处理、Swift限制等细节,并结合实际场景分析优化策略。掌握这些知识不仅能提升代码质量,还能在面试中展现深度技术理解。