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

发布于:2025-05-26 ⋅ 阅读:(27) ⋅ 点赞:(0)

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

  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 *)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

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. 动态生成子类并重写 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 回调

关键机制说明

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

参考文章

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


网站公告

今日签到

点亮在社区的每一天
去签到