iOS KVO

一、KVO 简述

KVO 全称 Key Value Observing,俗称“键值监听”;可以监听对象某个属性值的变化

1. KVO 是已什么方式实现的?(底层原理是什么?)

答:当对一个对象添加监听(addObserver:forKeyPath: ... ),iOS会修改该对象的 isa (isa默认指向对象所所属的类)。改为指向一个通过Runtime动态创建的子类,子类拥重写 set 方法,并且 set 方法内部会顺序调用 willChangeValueForKey, 原来的set方法,即:[super set...], didChangeValueForKey。并且会在 didChangeValueForKey 中调用KVO的回调方法:observeValueForKeyPath:ofObject:change:context:

2. 如何手动触发KVO?

答:已添加监听的属性,在值发生变化时,系统会自动触发回调。如果想要手动触发,则需我们自己调用 willChangeValueFor 和 didChallengeValueForKey方法,这两个方法缺一不可。

二、KVO 实现原理探索

1. 话不多说,上代码:
- (void)useSystemKVOTest {
    // 1\. 创建测试对象
    self.p1 = [Person new];
    self.p2 = [Person new];
    self.p1.age = 1;
    self.p2.age = 2;

    // 2\. 打印监听前p1、p2 所属类、setter 方法实现地址
    NSLog(@"监听前 p1 class is : %@, p2 class is : %@", object_getClass(self.p1), object_getClass(self.p2));
    // 输出结果:监听前 p1 class is : Person, p2 class is : Person
    NSLog(@"监听前 p1-setAage: address is : = %p, p2-setAage: address is : %p", [self.p1 methodForSelector:@selector(setAge:)], [self.p2 methodForSelector:@selector(setAge:)]);
    // 输出结果:监听前 p1-setAage: address is : = 0x102f98ea8, p2-setAage: address is : 0x102f98ea8

    // 3\. 添加监听,
    [self.p1 addObserver:self forKeyPath:NSStringFromSelector(@selector(age)) options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];

    // 4\. 打印监听后p1、p2 所属类、setter 方法实现地址
    NSLog(@"监听后 p1 class is : %@, p2 class is : %@", object_getClass(self.p1), object_getClass(self.p2));
    // 输出结果:监听后 p1 class is : NSKVONotifying_Person, p2 class is : Person
    NSLog(@"监听后 p1-setAage: address is : = %p, p2-setAage: address is : %p", [self.p1 methodForSelector:@selector(setAge:)], [self.p2 methodForSelector:@selector(setAge:)]);
    // 输出结果:监听后 p1-setAage: address is : = 0x194c61d54, p2-setAage: address is : 0x102f98ea8

    // 5\. 改变值
    self.p1.age = 10;
    self.p2.age = 20;

    // 6.移除 p1.age 的监听者
    [self.p1 removeObserver:self forKeyPath:NSStringFromSelector(@selector(age))];
}

// kvo 回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"监听到 %@ 的 %@ 改变了 %@", [object isEqual:self.p1]?@"p1":@"p2", keyPath, change);
    /* 输出结果:
     监听到 p1 的 age 改变了 {
         kind = 1;
         new = 10;
         old = 1;
     }
     */
}

image
2. 有以上输出结果,我们发现:
  • 在添加监听后,p1 的 isa 指向了 NSKVONotifying_Person
  • NSKVONotifyin_Person其实是Person的子类,那么也就是说其superclass指针是指向Person类对象的
  • NSKVONotifyin_Person 是 runtime 在运行时生成的。那么 p1 对象在调用 setage 方法的时候,肯定会根据 p1 的 isa 找到NSKVONotifyin_Person,在 NSKVONotifyin_Person 中找 setage 的方法及实现。
  • p1 的 setAge 方法的实现由 Person 类方法中的 setAge 方法转换为了C语言的 Foundation 框架的 _NSsetIntValueAndNotify 函数。
3. NSKVONotifyin_Person 的内部结构:

首先我们知道,NSKVONotifyin_Person作为Person的子类,其superclass指针指向Person类,并且NSKVONotifyin_Person内部一定对setAge方法做了单独的实现,那么NSKVONotifyin_Person同Person类的差别可能就在于其内存储的对象方法及实现不同。
我们通过runtime分别打印Person类对象和NSKVONotifyin_Person类对象内存储的对象方法

- (void)printMethods {
    [self printMehtodsOfClass:object_getClass(self.p1)];
    [self printMehtodsOfClass:object_getClass(self.p2)];
}

- (void)printMehtodsOfClass:(Class)cls {
    unsigned int count = 0;
    Method * methods = class_copyMethodList(cls, &count);    
    NSMutableString *methodNames = @"".mutableCopy;
    [methodNames appendFormat:@"%@ - ", cls];

    for (int i = 0; i < count; i++) {
        Method method = methods[i];
        NSString * methodName = NSStringFromSelector(method_getName(method));
        [methodNames appendString:methodName];
        [methodNames appendString:@"  "];
    }    
    NSLog(@"%@", methodNames);
    free(methods);
}

image

通过上述代码我们发现NSKVONotifyin_Person中有4个对象方法。分别为setAge: class dealloc _isKVOA,那么至此我们可以画出NSKVONotifyin_Person的内存结构以及方法调用顺序。

image

这里NSKVONotifyin_Person重写class方法是为了隐藏NSKVONotifyin_Person。不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1和p2对象的class可以发现他们都返回Person。

NSLog(@"%@, %@", [self.p1 class],  [self.p2 class]);
// 打印结果 Person, Person

三. 自定义 KVO 实现监听

1. ViewController 调用实现:
#import "ViewController.h"
#import "Person.h"
#import "NSObject+YJKVO.h"

@interface ViewController ()
@property (nonatomic, strong) Person * p;
@end

@implementation ViewController
#pragma mark - Life Cycle
- (void)viewDidLoad {
    [super viewDidLoad];
    [self useCustomKVOTest];
}

#pragma mark - 使用自定义kvo
- (void)useCustomKVOTest {
    self.p = [[Person alloc] init];
    [self.p yj_addObserver:self forKeyPath:NSStringFromSelector(@selector(name))];
    self.p.name = @"张三";
}

#pragma mark - 自定义kvo,回调
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object newValue:(id)newValue {
    NSLog(@"newValue = %@", newValue);
}

2. Person 类
  • Person.h
#import 

NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString * name;
@end

  • Person.m
#import "Person.h"

@implementation Person
- (void)setName:(NSString *)name {
    _name = name;
    NSLog(@"调用了");
}
@end

3. 定义一个 NSObject 的分类 NSObject+YJKVO,实现KVO监听
  • NSObject+YJKVO.h
@interface NSObject (YJKVO)
/// 添加观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/// 移除观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/// kvo 回调方法 (由观察者实现)
/// @param keyPath keyPath
/// @param object 被观察对象
/// @param newValue 新值
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object newValue:(id)newValue;
@end

  • NSObject+YJKVO.m
#import "NSObject+YJKVO.h"
#import 

// 通过 Runtime 动态成子类的前缀
static NSString *const YJKVOPrefix = @"YJKVO_";
// 关联 观察者
static NSString *const YJKVOAssociatedOberverKey = @"YJKVOAssociatedOberverKey";

@implementation NSObject (YJKVO)

#pragma mark - -- public methods
/// 添加观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {    
    // 1\. 检查时候有 set 方法
    NSString *setterMethodName = setterForGetter(keyPath);
    SEL setterSel = NSSelectorFromString(setterMethodName);
    // method
    Method method = class_getInstanceMethod(self.class, setterSel);
    if (!method) {
        @throw [[NSException alloc] initWithName:NSExtensionItemAttachmentsKey reason:@"没有setter方法" userInfo:nil];
    }

    // 2\. 动态生成子类
    Class sub_Class = [self registerSubClassWithKeyPath:keyPath];
    if (!sub_Class) {
        @throw [[NSException alloc] initWithName:NSExtensionItemAttachmentsKey reason:@"子类创建失败" userInfo:nil];
    }

    // 3\. 消息转发
    // 关联 observer
    objc_setAssociatedObject(self, (__bridge void const * _Nonnull)YJKVOAssociatedOberverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

/// 移除观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    objc_removeAssociatedObjects(observer);
}

/// kvo 回调方法 (由观察者实现)
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(nonnull id)object newValue:(nonnull id)newValue { }

#pragma mark - -- private methods
#pragma mark - 通过 getter 方法名,获取 setter 方法名;例如:age ==> setAge:
static NSString * setterForGetter(NSString *getter) {
    if (getter.length < 1) {
        return nil;
    }
    // 获取第一个字符,变成打下
    NSString *firstString = [[getter substringToIndex:1] uppercaseString]; // substringToIndex:从最前头一直截取到Index
    NSString *otherString = [getter substringFromIndex:1]; // substringFromIndex:从Index开始截取到最后
    // 拼接 age == > setAag:
    return [NSString stringWithFormat:@"set%@%@:", firstString, otherString];
}

#pragma mark - 通过 setter 方法名,获取 getter 方法名;例如:setAge: ==> age
static NSString * getterForSetter(NSString *setter) {
    if (setter.length < 1 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
        return nil;
    }
    NSString *getter = [setter substringFromIndex:3];
    getter = [getter substringToIndex:getter.length-1];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

#pragma mark - 动态生成子类
/// 运行时动态创建子类
/// @param keyPath keyPath
- (Class)registerSubClassWithKeyPath:(NSString *)keyPath {
    // 子类名
    NSString *subClsName = [NSString stringWithFormat:@"%@%@", YJKVOPrefix, self.class];
    // 子类,一个 NSObject 默认分贝 16 个字节
    Class subCls = objc_allocateClassPair(self.class, subClsName.UTF8String, 16);
    // 注册
    objc_registerClassPair(subCls);

    // 给子类动态添加 setter、class 实现
    Method class_method = class_getClassMethod(self.class, @selector(class));
    Method setter_method = class_getClassMethod(self.class, NSSelectorFromString(setterForGetter(keyPath)));
    class_addMethod(subCls, @selector(class), (IMP)yj_class, method_getTypeEncoding(class_method));
        class_addMethod(subCls,  NSSelectorFromString(setterForGetter(keyPath)), (IMP)yj_setter, method_getTypeEncoding(setter_method));

    // 将父类的 isa 指向子类
    object_setClass(self, subCls);
    // 返回
    return subCls;
}

#pragma mark - 重写 class 方法
static Class yj_class(id self, SEL _cmd) {
    return class_getSuperclass(object_getClass(self));
}

#pragma mark - 重写 setter 方法
/// 重写 setter 方法
/// @param newValue 新值
static void yj_setter(id self, SEL _cmd, id newValue) {    
    // 1\. 调用 super setter 方法
    struct objc_super super_cls = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    // 调用父类 setter 方法 设置新值
    ((void(*) (id, SEL, id)) (void *)objc_msgSendSuper)((__bridge id)(&super_cls), _cmd, newValue);

    // 2\. 取出观察者,调用kvo 回调方法
    id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(YJKVOAssociatedOberverKey));
    //
    SEL handleSel = @selector(yj_observeValueForKeyPath:ofObject:newValue:);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));

    // Runtime 调用回到方法
    // objc_msgSend() 默认的情况下,不支持添加参数。
    // 解决方案一: Build Setting –> 搜索: Enable Strict Checking of objc_msgSend Calls 改为 NO (我自己试了下,无效 Xcode12.1)
    // 解决方案二: 这里通过(void *)送入5个参数,你可以根据自己参数类型强转原本是void()的函数方法
    ((void (*) (id, SEL, NSString*, id, id)) (void*)objc_msgSend)(observer, handleSel, keyPath, self, newValue);
}
@end

image
  • 参考文章:iOS底层原理总结 - 探寻KVO本质

你可能感兴趣的:(iOS KVO)