欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 美食 > 「OC」源码学习——KVO底层原理探究

「OC」源码学习——KVO底层原理探究

2025/5/26 8:12:09 来源:https://blog.csdn.net/aa2002aa/article/details/148215991  浏览:    关键词:「OC」源码学习——KVO底层原理探究

「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用来处理有多个观察者的情况

  1. 同名键路径冲突:当多个对象或同一对象的不同属性使用相同的 keyPath 时,用 context 精准定位通知来源。
  2. 性能优化:通过指针地址直接匹配 context,避免字符串比较(keyPath 判断)的性能损耗。
  3. 安全性:防止父类与子类观察同一 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

image-20250524213554460

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";
}

img

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")];

image-20250525103650602

因为我们打印出来的都是存储在类本身的方法,所以显示出来的方法都是重写过后的方法,我们可以看到

NSKVONotifying_XiyouPerson中间类重写基类NSObjectclass 、 dealloc 、 _isKVOA方法

  • 其中dealloc是释放方法
  • _isKVOA判断当前是否是kvo类

dealloc之后isa的指向

我们在dealloc移除观察者之后,再观察isa指针的指向

img

我们可以看到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 回调

关键机制说明

  1. 动态子类:通过 objc_allocateClassPair 生成 CJLKVONotifying_Person 子类,并重写 class 方法隐藏自身。
  2. Setter 重写:在子类的 setName: 方法中调用父类实现后,遍历关联对象中的观察者信息,执行对应的 Block。
  3. 内存安全:使用弱引用 (weak) 持有观察者,避免循环引用;关联对象随被观察对象自动释放。

参考文章

iOS-底层原理 23:KVO 底层原理

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词