「OC」源码学习——KVO底层原理探究
文章目录
前言
之前在学习回调传值的时候,就对KVO进行了学习,接下来我们来继续进一步学习KVO之中的相关内容
KVO的基本使用
注册观察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@",change);
}
}
移除观察者
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
context的作用
context用来处理有多个观察者的情况
- 同名键路径冲突:当多个对象或同一对象的不同属性使用相同的
keyPath
时,用context
精准定位通知来源。 - 性能优化:通过指针地址直接匹配
context
,避免字符串比较(keyPath
判断)的性能损耗。 - 安全性:防止父类与子类观察同一
keyPath
时的逻辑混淆。
//定义context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
//注册观察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
//KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNickContext) {
NSLog(@"%@",change);
}else if (context == PersonNameContext){
NSLog(@"%@",change);
}
}
移除KVO的必要性
KVO 通过动态生成子类(如 NSKVONotifying_ClassName
)实现观察机制,子类会持有观察者的引用。若观察者未及时移除,动态子类无法释放,导致内存泄漏。
针对普通的对象,在释放的时候就自动移除了KVO,一般来说,不会出现问题。但是对于单例来说,因为他的内存不会被释放,如果重复仅需KVO观测的注册,很可能会造成内存溢出。
总的来说,KVO注册观察者 和移除观察者是需要成对出现的
KVO的手动触发和自动触发
我们可以通过重写automaticallyNotifiesObserversForKey
控制是否进行自动监听
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return YES;
}
手动触发
- (void)setName:(NSString *)name{
//手动开关
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
就是需要监听的时候进行手动调用这两个搭配的方法
KVO观察多个属性变化
监听多个属性变化核心其实就是实现keyPathsForValuesAffectingValueForKey
方法, 他是 KVO(键值观察)机制中的一个关键方法,用于 定义某个属性的值依赖于其他属性。当这些依赖属性发生变化时,系统会自动触发目标属性的 KVO 通知。
比如目前有一个需求,需要根据总的下载量totalData
和当前下载量currentData
来计算当前的下载进度currentProcess
,我们可以写出以下函数
[self.person addObserver:self forKeyPath:@"currentProcess" options:(NSKeyValueObservingOptionNew) context:NULL];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.person willChangeValueForKey:@"currentProcess"];
self.person.currentData += 10;
self.person.totalData += 2;
[self.person didChangeValueForKey:@"currentProcess"];
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"currentProcess"];
}
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"currentProcess"]) {
NSArray *affectingKeys = @[@"totalData", @"currentData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
NSLog(@"%@",keyPath);
if ([keyPath isEqualToString:@"currentProcess"]) {
// 处理属性变化逻辑(如更新 UI)
NSLog(@"New value: %@", change[NSKeyValueChangeNewKey]);
}
}
我们还要在这个person类之中,添加一下currentProcess
,让代码能够获取到currentProcess
的值
- (double)currentProcess {
if (self.totalData == 0) return 0;
return (self.currentData * 1.0 / self.totalData);
}
KVO观察 可变数组
对于数组来说,给数组添加元素并不会调用数组的setter方法,自然不会出发KVO的监听,那么我们通过对数组进行addObject
方法是无效的,那么我们要用以下的方法进行完成
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// KVC 集合 array
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
我们可以看出打印出来的context
kind
是一个枚举类型
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//设值
NSKeyValueChangeInsertion = 2,//插入
NSKeyValueChangeRemoval = 3,//移除
NSKeyValueChangeReplacement = 4,//替换
};
接下来讲一下mutableArrayValueForKey
这个函数
mutableArrayValueForKey
步骤 1:优先查找数组操作方法
方法名规则:
在对象的类中搜索以下方法(优先级顺序):insertObject:in<Key>AtIndex:
和removeObjectFrom<Key>AtIndex:
(对应NSMutableArray
的基础增删方法)insert<Key>:atIndexes:
和remove<Key>AtIndexes:
replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
(高性能替换方法)
触发条件:
若类实现了至少 一个插入方法 和 一个删除方法,则所有NSMutableArray
操作(如addObject:
、removeLastObject
)都会被 自动映射到这些自定义方法,确保数据同步和 KVO 通知。
步骤 2:退而使用 Setter 方法
- 方法名规则:
若未找到数组操作方法,则查找set<Key>:
方法。 - 触发条件:
每次通过代理对象修改数组时,会 生成新数组 并调用set<Key>: 方法
更新原属性。
性能问题:频繁生成新数组会导致性能损耗(需优先实现步骤 1 的方法优化)。
步骤 3:直接访问实例变量
- 变量名规则:
若accessInstanceVariablesDirectly
返回YES
,则按顺序查找实例变量_<key>
或<key>
。 - 触发条件:
代理对象直接操作实例变量(必须是NSMutableArray
或其子类实例),修改会 直接影响原数据 并触发 KVO 通知。
步骤 4:兜底异常处理
- 触发条件:
若上述方法均未找到,返回一个代理对象,但其操作会调用setValue:forUndefinedKey:
。 - 默认行为:
抛出NSUndefinedKeyException
异常。可通过重写setValue:forUndefinedKey:
自定义处理逻辑。
我们可以发现呢,按照这个流程来说,即使是NSArray,仍然可以插入内容,虽然说是将旧数组的数据复制再返回。
KVO观察属性与成员变量
我们知道KVO观察的是setter方法
self.person = [[Person alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
注册KVO
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
self.person.nickName = @"KC";
self.person->name = @"Cooci";
}
LLDB调试
object_getClassName
的本质是通过对象内存中的 isa
指针链访问类元数据,最终提取静态存储的类名字符串。我们发现,当我们给这个类添加KVO通知,isa指针就进行了变化
我们写一个函数,用于获取和person类相关的类
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
在一个函数来获取类之中的方法的函数
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
//********调用********
[self printClassAllMethod:objc_getClass("NSKVONotifying_XiyouPerson")];
因为我们打印出来的都是存储在类本身的方法,所以显示出来的方法都是重写过后的方法,我们可以看到
NSKVONotifying_XiyouPerson
中间类重写
了基类NSObject
的class 、 dealloc 、 _isKVOA
方法
- 其中
dealloc
是释放方法 _isKVOA
判断当前是否是kvo类
dealloc之后isa的指向
我们在dealloc移除观察者之后,再观察isa指针的指向
我们可以看到isa指针恢复原来的样子,而且在上一层控制器之中,我们打印Person的相关类,一样能找到这个变形的中间类,我们可以得出结论:中间类一旦生成,没有移除,不会销毁,还在内存中,主要可能还是考虑到复用的情景
自定义KVO
Block 回调的调用位置
1. 定义 KVO 核心类与模型
// JCKVO.h
#import <Foundation/Foundation.h>
typedef void(^JCKVOBLOCK)(id newValue, id oldValue);
@interface NSObject (JCKVO)
- (void)jc_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
handleBlock:(CJLKVOBLOCK)block;
@end
// JCKVO.m
#import "JCKVO.h"
#import <objc/runtime.h>
// 关联对象键
static NSString *const kJCKVOAssociateKey = @"kJCKVOAssociateKey";
// KVO 信息模型类
@interface JCKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) CJLKVOBLOCK handleBlock;
@end
@implementation JCKVOInfo
- (instancetype)initWithObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
handleBlock:(CJLKVOBLOCK)block {
if (self = [super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
// 动态子类前缀
static NSString *const kCJLKVOPrefix = @"JCKVONotifying_";
// 重写的 setter 实现
static void jc_setter(id self, SEL _cmd, id newValue) {
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
// 调用父类 setter(原类方法)
struct objc_super superCls = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
void (*msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
msgSendSuper(&superCls, _cmd, newValue);
// 触发 block 回调
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
for (JCKVOInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
info.handleBlock(newValue, oldValue);
}
}
}
2. 实现 KVO 注册逻辑
@implementation NSObject (JCKVO)
- (void)jc_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
handleBlock:(CJLKVOBLOCK)block {
// 1. 检查是否存在 setter 方法
Class superClass = object_getClass(self);
SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSelector);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:[NSString stringWithFormat:@"%@ 属性无 setter 方法", keyPath]
userInfo:nil];
}
// 2. 保存观察者信息
JCKVOInfo *info = [[JCKVOInfo alloc] initWithObserver:observer
forKeyPath:keyPath
handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
if (!mArray) {
mArray = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey),
mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
// 3. 动态生成子类并重写 setter
Class newClass = [self createChildClassWithKeyPath:keyPath];
object_setClass(self, newClass);
// 4. 添加 setter 方法
class_addMethod(newClass, setterSelector, (IMP)cjl_setter, method_getTypeEncoding(setterMethod));
}
// 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@", kCJLKVOPrefix, oldClassName];
Class newClass = NSClassFromString(newClassName);
if (newClass) return newClass;
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
objc_registerClassPair(newClass);
// 重写 class 方法以隐藏子类
SEL classSel = @selector(class);
Method classMethod = class_getInstanceMethod([self class], classSel);
class_addMethod(newClass, classSel, (IMP)cjl_class, method_getTypeEncoding(classMethod));
return newClass;
}
// 隐藏动态子类的 class 方法
Class jc_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
// 工具函数:getter -> setter
static NSString *setterForGetter(NSString *getter) {
if (getter.length <= 0) return nil;
NSString *firstLetter = [[getter substringToIndex:1] uppercaseString];
NSString *remaining = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:", firstLetter, remaining];
}
@end
3. 使用示例
// 被观察的类
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
// 使用 KVO Block
Person *person = [Person new];
[person cjl_addObserver:self
forKeyPath:@"name"
handleBlock:^(id newValue, id oldValue) {
NSLog(@"Name 变化: 旧值=%@, 新值=%@", oldValue, newValue);
}];
person.name = @"Alice"; // 触发 Block 回调
关键机制说明
- 动态子类:通过
objc_allocateClassPair
生成CJLKVONotifying_Person
子类,并重写class
方法隐藏自身。 - Setter 重写:在子类的
setName:
方法中调用父类实现后,遍历关联对象中的观察者信息,执行对应的 Block。 - 内存安全:使用弱引用 (
weak
) 持有观察者,避免循环引用;关联对象随被观察对象自动释放。