【iOS】源码学习-KVC与KVO
KVC
KVC全称Key-Value Coding,即键值编码。是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议来间接访问其属性。这种间接访问机制补充了实例变量及其相关的访问器方法锁提供的直接访问。
结合笔者自己的博客:【iOS】KVC
KVC相关API
常用四个API:
- 通过key设值:
- (void)setValue:(nullable id)value forKey:(NSString *)key; - 通过key取值:
- (nullable id)valueForKey:(NSString *)key; - 通过keyPath设值:
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; - 通过keyPath取值:
- (nullable id)valueForKeyPath:(NSString *)keyPath;
其他方法:
+ (BOOL)accessInstanceVariablesDirectly;:默认返回YES,表示如果没有找到set方法,会按照key,_iskey,key,iskey的顺序搜索成员。设置成NO就不这样搜索- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;:KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确,为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;:这是集合操作的API。如果属性是一个NSMutableArray,那么可以用这个方法来返回。- (nullable id)valueForUndefinedKey:(NSString *)key;:如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;:当用KVC赋值,传入的key在当前类中没有对应的属性、成员变量、存取方法时会自动触发该方法,直接抛出NSUnknownKeyException异常,程序崩溃。- (void)setNilValueForKey:(NSString *)key;:如果你在SetValue方法里面给value传nil,则会调用这个方法。- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;:输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
KVC支持基础数据类型和结构体,在使用KVC进行赋值或取值时,会自动在非对象值和对象值进行转换。
- 当进行取值时:如果返回值非对象,会使用该值初始化一个NSNumber(用于基础数据类型)或NSValue(用于结构体)实例,然后返回该实例。
- 当进行设值时:如果key的数据类型非对象,则会发送一条Value消息给value对象以提取基础数据,然后赋值给key。
KVC设值底层原理
当调用setValue:forKey:设值属性value时,其底层执行流程为:
- 首先按顺序查找是否有set、_set、setIs。如果有其中任意一个就直接设置属性的value;反之查找accessInstanceVariablesDirectly是否返回YES。
- 查找accessInstanceVariablesDirectly如果返回YES,则按顺序查找间接访问的实例变量
_<key>、_is、、is进行赋值。如果找到其中任意一个实例变量就赋值。 - 反之没找到任意一个或accessInstanceVariablesDirectly返回NO,系统都会执行该对象的
setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常。

KVC取值底层原理
当调用valueForKey:时,其底层的执行流程为:
- 首先按顺序查找是否有get、、is、_。如果找到就根据搜索到的属性值类型返回不同结果:
- 如果是对象指针,直接返回结果。
- 如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它。
- 如果是NSNumber不支持的标量类型,先转换为NSValue再返回该对象。
- 反之找不到就查找
countOf <Key>、objectIn <Key> AtIndex:和<key> AtIndexes:。如果找到了countOf <Key>和其他两个中的一个,则创建一个相应所有NSArray方法的集合代理对象,并返回该对象,即NSKeyValueArray(NSArray的子类)。代理对象随后接收到的所有NSArray消息转换为countOf <Key>、objectIn <Key> AtIndex:和<key> AtIndexes:消息的某种组合,用来创建KVC对象。如果原始对象还实现了get<Key>:range:之类的可选方法,则代理对象也将在适当时使用该方法。 - 如果这三个访问数组的都没有找到,则同时查找
countOf <Key>、enumeratorOf<Key>和memberOf<Key>三个方法。如果三个方法都找到,则会创建一个相应所有NSSet方法的集合代理对象,并返回该对象。此代理对象随后将其收到的所有NSSet消息转换为countOf <Key>、enumeratorOf<Key>和memberOf<Key>消息的某种组合,用于创建它的对象。 - 如果这三个方法还是没找到,就检查类方法
+accessInstanceVariablesDirectly是否返回YES。如果返回YES,则按照_<key>、_is<Key>、<key>、is<Key>顺序直接查找实例变量。 - 如果找到,就同1中根据搜索到的属性类型返回不同结果;反之,系统会执行该对象的
valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常。

KVC实际应用
- 动态设值和取值
可以通过setValue:forKey:和valueForKey:,也可以通过keyPath路由的方式setValue:forKeyPath:和valueForKeyPath:。
- 通过KVC访问和修改私有变量
OC每个实例对象地底层是isa指针+所有实例变量,无论公有、私有,全都存在对象内存中。而KVC的查找逻辑是在运行时Runtime按固定规则遍历,不区分@public、@protected、@private,只要变量名匹配,就直接读写内存。总结地说,普通点语法是编译器帮我们拦着私有变量,而KVC是绕开编译器,直接靠Runtime操作内存里的变量。
- 多值操作
即model和字典的互相转换,主要通过两个API实现。
- 字典转模型:
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues; - 模型转字典:
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- 修改一些系统空间的内部属性
内部UI控件苹果没有提供访问的API,KVC可以解决这个问题,如自定义tabbar,个性化UITextField中的placeHolderText。
- 实现高阶消息传递
普通情况,我们只能对单个对象发送消息,但是KVC可以允许我们对整个数组整体调用,会将消息发送给容器中的每个元素。
KVO
KVO全称Key-Value observing,即键值观察。它允许将其他对象的指定属性的更改通知给对象。KVC在对象创建完后,动态地给对象属性赋值,而KVO提供了一种监听机制。当指定对象的属性被修改后,对象会收到通知。
我们会发现KVO与NSNotificationCenter有点相似。
- 相同点:
- 两者的实现原理都是观察者模式,都用于监听。
- 都能实现一对多的操作。
- 不同点:
- KVO仅监听对象属性值变化,而通知可实现跨对象、跨页面、全局事件通信,不局限于属性。
- KVO由系统自动监听触发,属性一被修改就自动回调,而NSNotification完全由开发者手动控制,需要主动发生通知才会执行监听回调。
- KVO可直接在回调中获取属性新旧值,而通知默认不区分新旧值,需要开发者手动携带、解析参数。
KVO触发方式
- 自动触发
自动触发的开关为+ (BOOL)automicallyNotifiesObserversForKey:(NSString*)key。默认返回YES,表示开启。仅属性赋值会触发。直接修改底层成员变量或可变数组直接addObject等操作不调用setter方法,不会触发监听。
- 手动触发
重写类方法返回NO,关闭自动监听。开发者可在自定义setter方法中调用两个核心方法来自由控制监听触发时机。
objc
// 关闭自动监听
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
// 自定义setter,手动触发KVO
- (void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"]; // 标记属性即将变更
_name = name; // 赋值操作
[self didChangeValueForKey:@"name"]; // 标记属性变更完成,触发回调
}
KVO实际使用
KVO三大使用步骤
addObserver:forKeyPath:options:context::注册监听。观察者可以接受keyPath属性的变化事件。- observer:观察者,监听属性变化的对象
- keyPath:被观察的属性名称(字符串)
- options:监听配置,可获取新旧值(NSKeyValueObervingOptionNew、NSKeyValueObervingOptionOld)
- context:上下文指针,用于区分多个同名属性监听,优化回调判断,是KVO中的一致传值方式
observeValueForKeyPath:ofObject:change:context::实现回调方法。当keyPath属性发生改变时,KVO会回调这个方法通知观察者。- keyPath:被观察对象的属性
- object:被观察的对象
- change:字典,存放属性变更详情,根据options传入的枚举来返回新值旧值
- context:注册观察者时,context传递过来的值
removeObserver:forKeyPath::移除监听。当观察者不需要监听时,调用该方法将KVO移除。
KVO重复移除监听或者未及时移除都会导致崩溃:- 观察者销毁前没调用removeObserver:KVO底层强引用观察者。当被观察对象还存在,但观察者先被释放,系统仍会尝试向已销毁的观察者发送回调,访问野指针,直接崩溃。
- 同一keyPath重复调用removeObserver:每个keyPath的监听记录只会被移除一次。第一次移除,该ketPath监听记录被正常删除;第二次再移除同一个keyPath,系统去查找监听记录,发现不存在,直接抛出NSRangeException,程序崩溃。
KVO高阶用法
KVO实现一对多,即通过注册一个KVO观察者,可以监听多个属性的变化。
objc
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self runKVODemo];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.kvoPerson.currentData = 30;
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.kvoPerson.totalData = 200;
});
}
- (void)runKVODemo {
self.kvoPerson = [[LYDPerson alloc] init];
self.kvoPerson.totalData = 100;
self.kvoPerson.currentData = 0;
// 监听计算属性 currentProcess
[self.kvoPerson addObserver:self forKeyPath:@"currentProcess" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:LYDPersonCurrentProcessContext];
}
// KVO 回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
// 匹配当前监听的 context,防止干扰其他 KVO
if (context == LYDPersonCurrentProcessContext) {
CGFloat oldValue = [change[NSKeyValueChangeOldKey] floatValue];
CGFloat newValue = [change[NSKeyValueChangeNewKey] floatValue];
NSLog(@"currentProcess 发生变化 \n旧值: %.2f \n新值: %.2f", oldValue, newValue);
} else {
// 其他监听交给父类处理
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// dealloc 中安全移除 KVO,避免崩溃
- (void)dealloc {
@try {
[self.kvoPerson removeObserver:self forKeyPath:@"currentProcess"];
} @catch (NSException *exception) {
// 已提前移除则忽略,防止重复移除崩溃
}
}
@end
一个被监听属性currentProcess,多个依赖源属性currentData、totalData。这是典型的KVO实现一对多。
KVO特殊用法
KVO默认无法监听NSMutableArray内部元素增删,直接调用addObject或removeObject不会触发数组属性的setter方法,更不会触发KVO通知回调。KVC官方文档中的解决方案是通过mutableArrayValueForKey方法将元素添加到可变数组中。
objc
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupArrayKVO];
}
- (void)setupArrayKVO {
self.person = [[LYDPerson alloc] init];
self.person.dataArray = [NSMutableArray arrayWithObjects:@"A", @"B", nil];
// 监听数组,开启新值、旧值
[self.person addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:ArrayContext];
NSLog(@"无KVO回调");
[self.person.dataArray addObject:@"错误测试"];
// KVC包装操作,触发KVO
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"C"];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[self.person mutableArrayValueForKey:@"dataArray"] removeObject:@"A"];
});
}
// KVO回调:change由系统自动传入
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == ArrayContext) {
// 获取变更类型
NSNumber *kind = change[NSKeyValueChangeKindKey];
NSInteger changeKind = kind.integerValue;
// 获取变更下标(数组专用)
NSIndexSet *indexes = change[NSKeyValueChangeIndexesKey];
// 新值、旧值
id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];
NSLog(@"变更下标:%@", indexes);
NSLog(@"旧值:%@", oldValue);
NSLog(@"新值:%@", newValue);
// 区分四种变更状态
switch (changeKind) {
case NSKeyValueChangeSetting:
NSLog(@"类型:整体赋值");
break;
case NSKeyValueChangeInsertion:
NSLog(@"类型:插入元素");
break;
case NSKeyValueChangeRemoval:
NSLog(@"类型:删除元素");
break;
case NSKeyValueChangeReplacement:
NSLog(@"类型:替换元素");
break;
default:
break;
}
}
}
- (void)dealloc {
@try {
[self.person removeObserver:self forKeyPath:@"dataArray"];
} @catch (NSException *exception) {}
}
@end
同时数组变更的change字典中kind字段会区分插入、删除、替换、设值四种状态。

KVO底层原理
KVO是通过isa_swizzling技术实现的,全程依托Runtime动态操作类与对象。
当对象第一次添加观察者时,系统会动态创建一个NSKVONotifying_ 前缀的中间子类,并将当前对象的isa指针指向该子类。该子类会重写被监听属性的setter方法以及class、dealloc等方法。
当属性发生修改时,会先调用willChangeValueForKey:属性变更前通知,再执行父类原始setter方法完成赋值,最后调用didChangeValueForKey:属性变更后通知。didChangeValueForKey:方法内部会通知所有观察者,从而触发observeValueForKeyPath:ofObject:change:context:回调。
移除监听后,对象通常会恢复到原始类,Runtime创建的NSKVONotifying类交由系统管理并复用。

动态生成的NSKVONotifying_子类,会默认重写新增4个方法:
- 重写setter:拦截属性赋值,实现监听核心逻辑。
- 重写class:对外隐藏中间子类,返回原始类Class,屏蔽isa交换细节。
- 重写dealloc:处理监听销毁、资源回收。
- 私有方法_isKVOA:判断当前对象是否是KVO代理对象。
前面说到KVO使用isa-swizzling技术,苹果建议在开发中不应该依赖isa指针,而是通知class实例方法来获取对象类型。这是因为在运行时根据原类创建一个中间类,并动态修改当前对象的isa指向中间类,将class方法重写,返回原来类的class。KVO底层就依托isa-swizzling篡改对象isa指针实现功能。
Q:为什么要重写class方法?A:如果没有重写class方法,对象调用class时会遵循消息查找规则:先去当前的isa指向的KVO派生子类NSKVONotifying中依次查找方法缓存、方法列表,本类查找无果后,继续沿着父类继承链逐级向上查询父类的方法缓存与方法列表。因为class方法是NSObject中的方法,如果不重写最终可能会返回
NSKVONotifying_类名,就会将该类暴露出来。
KVO底层存储结构
系统通过三个核心类管理所有监听关系,保障线程安全与多层级存储:
- GSKVOInfo:用于保存和管理监控特定对象的观察者及其观察路径的信息,以支持KVO的实现。
objc
@interface GSKVOInfo : NSObject
{
NSObject *instance; // 被观察的对象实例,不被保留
NSRecursiveLock *iLock; // 递归锁,用于线程安全的访问
NSMapTable *paths; // 存储观察路径的映射表,用于保存keyPath到GSKVOPathInfo的映射
}
@end
它保存了一个对象的实例,但并不持有。因此在释放之后,再调用时会崩溃,需要在对象销毁前移除所有观察者。
- GSKVOPathInfo:用于记录某个键路径的观察者以及发送通知过程中的递归状态。
objc
@interface GSKVOPathInfo : NSObject
{
@public
unsigned recursion; // 递归计数器,跟踪发送通知的递归深度
unsigned allOptions; // 所有观察选项的位掩码,用于存储观察选项
NSMutableArray *observations; // 观察者数组,存储所有与键路径相关的观察者
NSMutableDictionary *change; // 变化字典,用于存储KVO触发后属性变化的信息
}
@end
- GSKVOObservation:用于记录单个观察的所有信息。
objc
@interface GSKVOObservation : NSObject
{
@public
NSObject *observer; // 观察者对象,不被保留(not retained)
void *context; // 上下文信息,用户在添加观察者时传递的上下文
int options; // 观察选项,指定观察的行为
}
@end
具体的调用流程:
- 添加观察者:当向一个对象添加观察者时,会创建或获取该对象对应的GSKVOInfo实例。在GSKVOInfo实例中,为特定的属性路径创建或获取一个GSKVOPathInfo实例,创建一个新的GSKVOObservation实例,存储观察者的信息,并将其添加到GSKVOPathInfo的observations数组中。
- 发送通知:当属性值发生变化时,GSKVOPathInfo实例会遍历其observations数组,向所有观察者发送通知。在通知发送过程中,会更新recursion属性以跟踪递归状态,确保线程安全。
- 移除观察者:当移除观察者时,会从GSKVOPathInfo实例的observations数组中删除相应的GSKVOObservation实例。如果某个属性路径不再有观察者,则从GSKVOInfo实例的paths表中移除对应的GSKVOPathInfo实例。

KVO底层链路
isa-swizzling技术主要通过以下几个类实现:
这里看GNUstep的KVO实现源码。
- GSKVOReplacement:专门记录某个类被KVO监听后,Runtime动态生成出来的新子类的信息。
objc
@interface GSKVOReplacement : NSObject
{
Class original; // 保存被观察对象的原始类
Class replacement; // 保存替代原始类的中间类,即NSKVONotifying_XXX,以便拦截和处理属性变化通知
NSMutableSet *keys; // 保存所有被观察的属性键集合,表示哪些属性被观察者监听
}
- (id) initWithClass: (Class)aClass;
- (void) overrideSetterFor: (NSString*)aKey;
- (Class) replacement;
@end
这里具体看一下初始化方法:
该类接受一个类作为参数。使用NSStringFromClass函数获取原始类的类名,并生成一个新的类名,使用GSObjcMakeClass函数创建一个继承自原始类的新的类模版,
objc
- (id) initWithClass: (Class)aClass
{
NSValue *template;
NSString *superName;
NSString *name;
original = aClass;
/*
* Create subclass of the original, and override some methods
* with implementations from our abstract base class.
*/
// 获取原始类的类名,并生成一个新的类名
superName = NSStringFromClass(original); // original == Temp
name = [@"GSKVO" stringByAppendingString: superName]; // name = GSKVOTemp
// 创建一个继承自原始类的新的类模版
template = GSObjCMakeClass(name, superName, nil); // template = GSKVOTemp
// 注册新的替代类并获取其class对象
GSObjCAddClasses([NSArray arrayWithObject: template]);
replacement = NSClassFromString(name);
// 为替代类添加行为,通常是方法实现
GSObjCAddClassBehavior(replacement, baseClass);
/* Create the set of setter methods overridden.
*/
// 初始化keys集合,用于存储被重写的setter方法并返回初始化后的实例
keys = [NSMutableSet new];
return self;
}
- GSKVOBase:该类默认提供了几个重写NSObject方法的方法。这些方法都会拷贝到新创建的替换类中,被观察者拥有。
objc
@implementation GSKVOBase
- (void) dealloc
{
// Turn off KVO for self ... then call the real dealloc implementation.
// 对象释放后,移除KVO数据,将对象重新指向原始类
[self setObservationInfo: nil];
object_setClass(self, [self class]);
[self dealloc];
GSNOSUPERDEALLOC;
}
// 此方法用来隐藏替换类信息,应用层获取类的信息仍然是原始类的信息。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
- (Class) class
{
return class_getSuperclass(object_getClass(self));
}
// KVO通知触发的核心方法
// 重写KVC中的相关方法,在属性值发生变化前后分别调用[self willChangeValueForKey:aKey]和[self didChangeValueForKey:aKey]
// 这是触发KVO通知的关键,KVO是基于KVC的,KVC正是KVO触发的入口
- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
Class c = [self class];
void (*imp)(id,SEL,id,id);
imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];
if ([[self class] automaticallyNotifiesObserversForKey: aKey])
{
[self willChangeValueForKey: aKey];
imp(self,_cmd,anObject,aKey);
[self didChangeValueForKey: aKey];
}
else
{
imp(self,_cmd,anObject,aKey);
}
}
// 此方法和class方法原理相同
- (Class) superclass
{
return class_getSuperclass(class_getSuperclass(object_getClass(self)));
}
@end
- GSKVOSetter:是KVO实现中的setter代理方法。
KVO监听属性后,需要重写属性对应的setter方法。GNUstep没有为每个属性动态生成独立的setter实现,而是提供了一组通用setter(如setterInt、setterFloat等),然后通过Runtime将被观察属性的setter IMP替换为这些方法。
这些方法内部会先调用willChangeValueForKey:,再执行原setter方法,最后调用didChangeValueForKey:,进而完成KVO通知的发送。
objc
@interface GSKVOSetter : NSObject
- (void) setter: (void*)val;
- (void) setterChar: (unsigned char)val;
- (void) setterDouble: (double)val;
- (void) setterFloat: (float)val;
- (void) setterInt: (unsigned int)val;
- (void) setterLong: (unsigned long)val;
#ifdef _C_LNG_LNG
- (void) setterLongLong: (unsigned long long)val;
#endif
- (void) setterShort: (unsigned short)val;
- (void) setterRange: (NSRange)val;
- (void) setterPoint: (NSPoint)val;
- (void) setterSize: (NSSize)val;
- (void) setterRect: (NSRect)rect;
@end
自定义KVO跟系统一致,只是在系统的基础上做了一些优化。
- 将注册和响应通过函数式编程,即block的方法结合在一起。
在系统中,注册观察者和KVO响应属于响应式编程,是分开写的。在自定义KVO中为了代码更好的协调,采用block的形式,将注册和回调的逻辑组合在一起,即函数式编程。- 去掉系统繁琐的注册观察者、KVO响应、移除观察者三部曲,实现KVO自动销毁机制