一、KVC(Key-Value Coding)的底层行为
1. KVC 赋值流程(setValue:forKey:
)
graph TD
A[调用 setValue:forKey:] --> B{是否存在 setKey: 或 _setKey: 方法?}
B -->|是| C[调用对应方法]
B -->|否| D[检查 +accessInstanceVariablesDirectly]
D -->|YES| E[按顺序查找成员变量 _key -> _isKey -> key -> isKey]
E --> F[找到则赋值]
D -->|NO| G[抛出 NSUnknownKeyException]
2. KVC 取值流程(valueForKey:
)
graph TD
A[调用 valueForKey:] --> B{是否存在 getKey/key/isKey/_key 方法?}
B -->|是| C[调用对应方法]
B -->|否| D[检查 +accessInstanceVariablesDirectly]
D -->|YES| E[按顺序查找成员变量 _key -> _isKey -> key -> isKey]
E --> F[找到则返回值]
D -->|NO| G[抛出 NSUnknownKeyException]
二、KVC 的底层行为与实现细节
1. setValue:forKey:
赋值流程详解
当调用 setValue:forKey:
时,KVC 按以下顺序查找并执行赋值操作:
方法优先查找
set<Key>:
查找与属性名匹配的 setter 方法(如属性age
对应setAge:
)。_set<Key>:
若未找到set<Key>:
,则查找带下划线的 setter 方法(如_setAge:
)。
成员变量次之
若未找到上述方法,检查 +accessInstanceVariablesDirectly
方法的返回值:
- 返回 YES :按顺序查找成员变量:
_<key>
→_is<Key>
→<key>
→is<Key>
(例如_age
→_isAge
→age
→isAge
)。
- 返回 NO :直接抛出
NSUnknownKeyException
异常。
赋值结果处理
- 找到成员变量:直接赋值。
- 未找到:抛出异常。
objc
// 示例:通过 KVC 设置私有成员变量
@interface Person : NSObject {
@private
NSString *_nickname;
}
@end
Person *p = [[Person alloc] init];
[p setValue:@"Jack" forKey:@"nickname"]; // 成功赋值 _nickname
2. valueForKey:
取值流程详解
当调用 valueForKey:
时,KVC 按以下顺序查找并返回值:
方法优先查找
get<Key>
查找与属性名匹配的 getter 方法(如getName
)。<key>
直接查找与属性名相同的方法(如name
)。is<Key>
查找布尔类型的 getter(如isActive
对应属性active
)。_<key>
查找带下划线的 getter(如_name
)。
成员变量次之
若未找到上述方法,检查 +accessInstanceVariablesDirectly
方法的返回值:
- 返回 YES :按顺序查找成员变量:
_<key>
→_is<Key>
→<key>
→is<Key>
(例如_age
→_isAge
→age
→isAge
)。
- 返回 NO :抛出
NSUnknownKeyException
异常。
取值结果处理
- 找到成员变量:返回其值。
- 未找到:抛出异常。
objc
// 示例:通过 KVC 获取私有成员变量
NSString *nickname = [p valueForKey:@"nickname"]; // 返回 _nickname 的值
3. KVC 方法查找与成员变量访问对比
操作类型 | 方法查找顺序 | 成员变量查找顺序 | 权限控制方法 |
---|---|---|---|
赋值 | setKey: → _setKey: |
_key → _isKey → key → isKey |
+accessInstanceVariablesDirectly |
取值 | getKey → key → isKey → _key |
_key → _isKey → key → isKey |
+accessInstanceVariablesDirectly |
4. 常见场景与陷阱
场景 1:布尔类型属性的特殊处理
若属性为布尔类型(如 isActive
),valueForKey:@"active"
会优先调用 isActive
方法:
objc
@interface User : NSObject
@property (nonatomic, assign) BOOL isActive;
@end
User *user = [[User alloc] init];
user.isActive = YES;
NSNumber *isActive = [user valueForKey:@"active"]; // 调用 isActive 方法
场景 2:集合类型属性的 KVC 访问
若属性为集合(如 NSArray
),valueForKey:
可能触发以下方法:
countOf<Key>
:返回集合元素数量。objectIn<Key>AtIndex:
:根据索引获取元素。
objc
@interface Team : NSObject
- (NSUInteger)countOfMembers;
- (id)objectInMembersAtIndex:(NSUInteger)index;
@end
Team *team = [[Team alloc] init];
NSArray *members = [team valueForKey:@"members"]; // 触发上述方法
陷阱:直接访问私有成员变量
若未实现 getter/setter 且 accessInstanceVariablesDirectly
返回 NO
,直接访问成员变量会崩溃:
objc
@interface SecretData : NSObject {
@private
NSString *_password;
}
@end
@implementation SecretData
+ (BOOL)accessInstanceVariablesDirectly {
return NO; // 禁止 KVC 访问成员变量
}
@end
SecretData *data = [[SecretData alloc] init];
[data setValue:@"123456" forKey:@"password"]; // 抛出 NSUnknownKeyException
5. 性能优化建议
- 避免频繁使用 KVC:方法查找和消息转发会带来额外开销,性能敏感场景优先使用直接方法调用。
- 缓存 Key Paths:若需多次访问同一属性,可将 Key Path 转换为指针或常量减少字符串解析开销。
- 合理控制访问权限 :通过重写
+accessInstanceVariablesDirectly
限制不必要的成员变量暴露。
三、面试题扩展
Q:valueForKey:
和 objectForKey:
有何区别?
valueForKey:
属于 KVC 方法,通过属性名或键路径查找值,支持方法调用和成员变量访问,可触发 KVO。objectForKey:
是NSDictionary
的方法,直接从字典中根据键取值,不涉及 KVC/KVO 机制。
Q:如何阻止 KVC 访问私有成员变量?
重写 +accessInstanceVariablesDirectly
返回 NO
:
objc
+ (BOOL)accessInstanceVariablesDirectly {
return NO; // 禁止 KVC 直接访问成员变量
}