文章目录
KVC设值流程
当我们调用setValue:forKey:设置属性时,底层执行流程如下:
- 【第一步】先查找是否有三种setter方法,顺序为
set<Key>: -> _set<Key> -> setIs<Key>- 如果有任意一个setter方法,则直接设置属性的value
- 如果都没有的话,直接进入第二步
- 【第二步】如果第一步中的setter方法都没有,就查找
accessInstanceVariablesDirectly是否返回YES- 如果返回yes,则查找间接访问的时机变量进行赋值,查找顺序为
_<key> -> _is<Key> -> <key> -> is<Key>- 如果找到其中任意一个实例变量,则赋值
- 如果都没有就进入第三步
- 如果返回NO,直接进入第三步
- 如果返回yes,则查找间接访问的时机变量进行赋值,查找顺序为
- 【第三步】如果setter方法或者实例变量都没有找到,系统会执行该对象的
setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常
KVC取值流程
-
【第一步】首先查找 getter 方法,按照如下顺序查找
get<Key>,<key>,is<Key>,_get<Key>- 如果找到,则调用该方法取得返回值,并进入【第五步】。
- 如果没有找到,则进入【第二步】。
-
【第二步】如果第一步中的 getter 方法没有找到,KVC 会查找数组访问方法,按照以下规则判断是否支持 NSArray 风格的集合访问:
countOf<Key>,objectIn<Key>AtIndex:,<key>AtIndexes:- 如果找到了
countOf<Key>,并且找到了objectIn<Key>AtIndex:或<key>AtIndexes:其中之一,则 KVC 会创建一个 NSArray 风格的集合代理对象。该代理对象会将接收到的 NSArray 相关消息,转换为对原对象的:countOf<Key>,objectIn<Key>AtIndex:,<key>AtIndexes:,get<Key>:range:
等方法的调用。
-
其中
get<Key>:range:是可选的批量访问方法,如果对象实现了它,代理对象会在合适的时候使用它。 -
如果没有满足数组集合访问方法的要求,则进入【第三步】。
- 如果找到了
-
【第三步】如果没有找到数组访问方法,KVC 会继续查找 Set 集合访问方法:
countOf<Key>,enumeratorOf<Key>,memberOf<Key>:- 如果这三个方法都找到了,则 KVC 会返回一个 NSSet 风格的集合代理对象。该代理对象会将接收到的 NSSet 相关消息,转换为对原对象的:
countOf<Key>,enumeratorOf<Key>,memberOf<Key>:
等方法的调用。
- 如果仍然没有找到,则进入【第四步】。
- 如果这三个方法都找到了,则 KVC 会返回一个 NSSet 风格的集合代理对象。该代理对象会将接收到的 NSSet 相关消息,转换为对原对象的:
-
【第四步】如果以上方法都没有找到,KVC 会检查类方法:
+ accessInstanceVariablesDirectly-
如果该方法返回
YES,则 KVC 会按照以下顺序直接查找实例变量:_<key>,_is<Key>,<key>,is<Key> -
如果找到对应实例变量,则直接读取该实例变量的值,并进入【第五步】。
-
如果
+ accessInstanceVariablesDirectly返回NO,或者没有找到对应实例变量,则进入【第六步】。
-
-
【第五步】根据取得的值的类型,返回不同结果:
-
如果取得的是对象指针,则直接返回该对象。
-
如果取得的是
NSNumber支持的标量类型,例如int,float,double,BOOL,NSInteger,NSUInteger则 KVC 会将其包装成NSNumber对象返回。 -
如果取得的是结构体等非对象类型,例如:
CGRect,CGPoint,CGSize,NSRange则 KVC 会将其包装成NSValue对象返回。
-
-
【第六步】如果以上步骤全部失败,KVC 会调用:
valueForUndefinedKey:默认实现会抛出:
NSUndefinedKeyException异常。
KVC使用场景
- 动态设值和取值
- 可以通过
setValue:forKey:和valueForKey: - 也可以通过
路由的方式setValue:forKeyPath:和valueForKeyPath:
- 通过KVC反问和修改私有变量
- 日常开发中对于类的私有属性,外部定义的对象是没办法直接访问的,但是对于KVC而言,可以访问和修改任何私有属性
- 多值操作(model和字典互转化)
具体实现如下
objc
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface BaseModel : NSObject
// 字典 → 模型
- (instancetype)initWithDictionary:(NSDictionary *)dict;
+ (instancetype)modelWithDictionary:(NSDictionary *)dict;
// 模型 → 字典
- (NSDictionary *)toDictionary;
// key 映射(可选)
+ (NSDictionary *)modelCustomPropertyMapper;
// 数组元素类型(可选)
+ (NSDictionary *)modelContainerPropertyGenericClass;
@end
NS_ASSUME_NONNULL_END
//实现文件
#import "BaseModel.h"
#import <objc/runtime.h>
@implementation BaseModel
#pragma mark - 初始化
- (instancetype)initWithDictionary:(NSDictionary *)dict {
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
+ (instancetype)modelWithDictionary:(NSDictionary *)dict {
return [[self alloc] initWithDictionary:dict];
}
#pragma mark - KVC 核心
- (void)setValue:(id)value forKey:(NSString *)key {
if (!key) return;
// 1. key 映射
NSDictionary *mapper = [[self class] modelCustomPropertyMapper];//获取映射表
NSString *propertyKey = mapper[key] ?: key;//看看映射表中有没有对应的映射
objc_property_t property = class_getProperty([self class], propertyKey.UTF8String);//判断类中是否真的有这个属性
if (!property) return;
const char *attrs = property_getAttributes(property);//获取属性的编码字符串
NSString *attrStr = [NSString stringWithUTF8String:attrs];//将C字符串转换为NSString
// 2. 对象类型处理
if ([attrStr hasPrefix:@"T@"]) {
// 获取类名 @"ClassName"
NSRange start = [attrStr rangeOfString:@"@\""];
if (start.location != NSNotFound) {//属性是OC类型
NSRange end = [attrStr rangeOfString:@"\"" options:0 range:NSMakeRange(start.location+2, attrStr.length-start.location-2)];
NSString *clsName = [attrStr substringWithRange:NSMakeRange(start.location+2, end.location-start.location-2)];
Class cls = NSClassFromString(clsName);
// 3. 嵌套模型
if ([value isKindOfClass:[NSDictionary class]] && [cls isSubclassOfClass:[BaseModel class]]) {
value = [[cls alloc] initWithDictionary:value];
}
}
// 4. 数组模型
if ([value isKindOfClass:[NSArray class]]) {
NSDictionary *generic = [[self class] modelContainerPropertyGenericClass];
Class itemCls = generic[propertyKey];
if (itemCls) {
NSMutableArray *arr = [NSMutableArray array];
for (id obj in value) {
if ([obj isKindOfClass:[NSDictionary class]]) {
id model = [[itemCls alloc] initWithDictionary:obj];
[arr addObject:model];
} else {
[arr addObject:obj];
}
}
value = arr;
}
}
}
[super setValue:value forKey:propertyKey];
}
#pragma mark - 容错
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
// 忽略未知 key(避免 crash)
}
- (void)setNilValueForKey:(NSString *)key {
// 防止基本类型为 nil crash
}
#pragma mark - 模型 → 字典
- (NSDictionary *)toDictionary {
unsigned int count = 0;
objc_property_t *properties = class_copyPropertyList([self class], &count);
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < count; i++) {
const char *name = property_getName(properties[i]);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
if (!value) continue;
// 嵌套模型
if ([value isKindOfClass:[BaseModel class]]) {
value = [value toDictionary];
}
// 数组模型
if ([value isKindOfClass:[NSArray class]]) {
NSMutableArray *arr = [NSMutableArray array];
for (id obj in value) {
if ([obj isKindOfClass:[BaseModel class]]) {
[arr addObject:[obj toDictionary]];
} else {
[arr addObject:obj];
}
}
value = arr;
}
dict[key] = value;
}
free(properties);
return dict;
}
#pragma mark - 默认实现
+ (NSDictionary *)modelCustomPropertyMapper {
return @{};
}
+ (NSDictionary *)modelContainerPropertyGenericClass {
return @{};
}
@end
如果我们想使用这个方法,模型就需要继承自BaseModel,并且重写对应的\+ (NSDictionary *)modelCustomPropertyMapper与modelContainerPropertyGenericClass
使用:
objc
UserModel* model = [UserModel modelWithDictionary:dict];
- 修改一些系统空间的内部属性
在日常开发中,我们知道很多UI控件的内部都是由多个UI控件组合而成,这些内部控件是没有提供访问的API的,这里就可以使用KVC。
- 使用KVC实现高阶消息传递
普通情况,我们只能对单个对象发送消息,但是KVC可以允许我们对整个数组整体调用,会将消息发送给数组中的每一个元素
KVO
前言
- KVC是键值编码,在对象创建完成之后可以动态的给对象属性赋值。
- KVO是键值观察,提供监听机制,当指定对象的属性被修改之后,则对象会收到通知。
KVO与NSNotification的区别
- 相同点
- 都是观察者模式,用于监听
- 都能实现1对多
- 不同点
- KVO只能用于监听对象属性的变化,并且属性名都是通过NSString来查找,编译器不会检查输入是否错误
- NSNotification的发送通知操作我们可以控制,KVO是由系统控制
- KVO可以记录新旧值变化
基本使用
- 注册观察者
addObserver:forKeyPath:options:context - 实现KVO回调
observeValueForKeyPath:ofObject:change:context - 移除观察者
removeObserver:forKeyPath:context
对于context参数,可以用来区分不同对象的同名属性,从而在KVO的回调中可以直接使用context进行区分,可以大幅度提高性能,以及代码的可读性。
objc
static void *ZYLPersonNickNameContext = &ZYLPersonNickNameContext;
- (void)runKVODemo {
self.kvoPerson = [ZYLPerson new];
self.kvoPerson.nickName = @"oldName";
[self.kvoPerson addObserver:self
forKeyPath:@"nickName"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:ZYLPersonNickNameContext];
self.kvoPerson.nickName = @"newName";
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if (context == ZYLPersonNickNameContext) {
NSLog(@"KVO keyPath: %@", keyPath);
NSLog(@"old: %@", change[NSKeyValueChangeOldKey]);
NSLog(@"new: %@", change[NSKeyValueChangeNewKey]);
return;
}
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
即使移除观察者
移除观察者前必须确定当前对象是观察者,否则会抛出NSRangeException。
观察者的注册和移除需要成对出现,避免重复注册之类的问题。如果旧的观察者没有释放,那么就会出现重复通知的问题。但是如果旧的观察者释放了的话,KVO系统中还保留着一条旧的观察关系,但是这个旧的观察关系指向了一个可能已经释放的观察者,出出现类似于野指针的崩溃。
自动触发与手动触发
- 自动开关
objc
+ (BOOL)automicallyNotifiesObserversForKey:(NSString*)key {
return YES;返回
}
- 手动开关:当自动开关关闭的时候,可以通过手动开关监听
objc
- (void)setNickName:(NSString *)nickName {
[self willChangeValueForKey:@"nickName"];
_nickName = nickName;
[self didChangeValueForKey:@"nickName"];
}

KVO实现一对多
示例如下:
objc
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) CGFloat currentData;
@property (nonatomic, assign) CGFloat totalData;
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {//一个系统方法,设置依赖
NSSet* keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"currentProcess"]) {
NSArray* affectingKeys = @[@"totalData", @"currentData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (CGFloat)currentProcess {
if (self.totalData <= 0) {
return 0;
}
return self.currentData / self.totalData;
}
- (void)runKVODemo2 {
self.kvoPerson = [[ZYLPerson alloc] init];
self.kvoPerson.totalData = 100;
self.kvoPerson.currentData = 0;
[self.kvoPerson addObserver:self
forKeyPath:@"currentProcess"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:ZYLPersonCurrentProcessContext];
}
KVO观察可变数组
在KVC官方文档中,针对可变数组的集合类型,有如下说明,即访问集合对象需要需要通过mutableArrayValueForKey方法,这样才能将元素添加到可变数组中
objc
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"0"];
KVO底层探索
- KVO是使用isa-swizzling技术实现的
- isa指向了一个类,内部维护者一张表,这个表中有着类的方法是实现以及一些其他数据(bits->data)即class_rw_t
- 当对象的属性被注册为观察者之后,对象的isa就会被修改指向一个中间类。(所以尽量不要使用isa找类,而是使用class方法)
验证
我们打上断点验证如下:

可以发现两次输出的self.kvoPerson属性所属的类是不同的,self.kvoPerson的isa指针指向了一个中间类。
中间类是啥
可以发现中间类的命名规范是:NSKVONotifying_实例的类名
我们先猜测中间类是原类的子类,因为KVO并未影响对象的setter方法,只是添加了监听,所以猜测是子类重写了setter方法,在其中添加了监听。

通过添加allChildClassAndSelf:方法,可以发现KVO再给对象属性注册观察者之后,对象的isa指向了一个中间类,并且这个中间类是对象本身的类的子类。


我们通过调用 allMethodsForClass:方法可以发现,KVO重写的是属性的set方法对于没有set方法的实例变量是不会观察的。
另外,我们可以发现中间类还重写了class方法,这是为了确保后续对象调用class方法还是会得到对象原有的类。
在移除观察者之后,isa指回原来的类
objc
- (void)dealloc {
@try {
[self.kvoPerson removeObserver:self forKeyPath:@"currentProcess" context:ZYLPersonCurrentProcessContext];
[self allChildClassAndSelf:[ZYLPerson class]];
} @catch (__unused NSException *exception) {
}
@try {
[self.kvoPerson removeObserver:self forKeyPath:@"nickName" context:ZYLPersonNickNameContext];
} @catch (__unused NSException *exception) {
}
}
我们在移除观察者之后再次调用allChildClassAndSelf:方法,发现中间类一旦生成,没有移除、没有销毁,还在内存中。这是基于虚重用的想法。
总结
- 实例对象
isa的指向在注册KVO观察者之后,由原有类更改为指向中间类 中间类重写了观察属性的setter方法、class、dealloc、_isKVOA方法- dealloc方法中,移除KVO观察者之后,实例对象
isa指向由中间类更改为原有类 中间类从创建后,就一直存在内存中,不会被销毁
自定义KVO
具体代码这里就不粘贴了,大概讲解一下流程:
- 注册观察者&响应
- 验证是否存在sette方法
- 保存信息
- 动态生成子类,重写class、setter方法
- 在子类的setter方法中向父类发送消息
- 让观察者响应
- 移除观察者
- 更改isa指向原有类
- 重写子类的dealloc方法