「OC」源码学习——KVO底层原理探究
文章目录
- 「OC」源码学习——KVO底层原理探究
- 前言
- KVO的基本使用
- context的作用
- 移除KVO的必要性
- KVO的手动触发和自动触发
- KVO观察多个属性变化
- KVO观察 可变数组
- mutableArrayValueForKey
- **步骤 1:优先查找数组操作方法**
- **步骤 2:退而使用 Setter 方法**
- **步骤 3:直接访问实例变量**
- **步骤 4:兜底异常处理**
- KVO观察属性与成员变量
- LLDB调试
- dealloc之后isa的指向
- 自定义KVO
- **Block 回调的调用位置**
- **1. 定义 KVO 核心类与模型**
- **2. 实现 KVO 注册逻辑**
- **3. 使用示例**
- **关键机制说明**
- 参考文章
前言
之前在学习回调传值的时候,就对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 *)keyPathofObject:(id)objectchange:(NSDictionary *)changecontext:(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. 动态生成子类并重写 setterClass 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
) 持有观察者,避免循环引用;关联对象随被观察对象自动释放。
参考文章
iOS-底层原理 23:KVO 底层原理