KVO底层原理

252 阅读7分钟

KVO概念

KVO全称KeyValueObserving,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。

KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而NSNotificationCenter是一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。

KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。

KVO底层原理探索

原理探索都是基于这段代码

//  Person.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END

//  Person.m
#import "Person.h"

@implementation Person

@end

//  NHViewController.m
#import "NHViewController.h"
#import <objc/runtime.h>
#import "Person.h"

@interface NHViewController ()
@property (nonatomic, strong) Person* person;
@end

@implementation NHViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.person = [[Person alloc] init];
    NSLog(@"---- object_getClass(self.person) : %@ ----",NSStringFromClass(object_getClass(self.person)));
    [self printfClassMethod:object_getClass(self.person)];
    NSLog(@"[self.person class] : %@",NSStringFromClass([self.person class]));
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    NSLog(@"---- object_getClass(self.person) : %@ ----",NSStringFromClass(object_getClass(self.person)));
    [self printfClassMethod:object_getClass(self.person)];
    NSLog(@"[self.person class] : %@",NSStringFromClass([self.person class]));
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.name = @"Noah";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"object:%@ -- change:%@",object,change);
}

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"name"];
}

#pragma mark 打印类的全部方法

- (void)printfClassMethod:(Class)class{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(class, &count);
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = method_getImplementation(method);
        NSLog(@"SEL:%@ -- IMP:%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}
@end

  • 首先我们打个断点在self.person = [[Person alloc] init];这一行
  • 进入断点之后用lldb调试,打印出当前对象isa指针指向的类,添加观察者之前先打印一次
  • 添加观察者之后再次打印一次
    可以看出,添加观察者之后,实例对象的isa指向发生了改变,接下来我们来看看类里面的方法,把注释打开
    打印如下:
2020-01-22 18:48:39.121268+0800 KVO_Demo[3593:148194] ---- object_getClass(self.person) : Person ----
2020-01-22 18:48:39.121386+0800 KVO_Demo[3593:148194] SEL:.cxx_destruct -- IMP:0x106e37e20
2020-01-22 18:48:39.121450+0800 KVO_Demo[3593:148194] SEL:name -- IMP:0x106e37db0
2020-01-22 18:48:39.121492+0800 KVO_Demo[3593:148194] SEL:setName: -- IMP:0x106e37de0
2020-01-22 18:48:39.121770+0800 KVO_Demo[3593:148194] ---- object_getClass(self.person) : NSKVONotifying_Person ----
2020-01-22 18:48:39.121818+0800 KVO_Demo[3593:148194] SEL:setName: -- IMP:0x107192d1a
2020-01-22 18:48:39.121875+0800 KVO_Demo[3593:148194] SEL:class -- IMP:0x10719174e
2020-01-22 18:48:39.121917+0800 KVO_Demo[3593:148194] SEL:dealloc -- IMP:0x1071914f2
2020-01-22 18:48:39.121963+0800 KVO_Demo[3593:148194] SEL:_isKVOA -- IMP:0x1071914ea

可以看到,NSKVONotifying_Person这个类重写了setName,class,dealloc方法

  • 接下来我们看看class方法的返回值,把注释打开
    打印:
2020-01-22 19:14:27.928370+0800 KVO_Demo[3830:156153] ---- object_getClass(self.person) : Person ----
2020-01-22 19:14:27.928448+0800 KVO_Demo[3830:156153] SEL:.cxx_destruct -- IMP:0x108bbee20
2020-01-22 19:14:27.928496+0800 KVO_Demo[3830:156153] SEL:name -- IMP:0x108bbedb0
2020-01-22 19:14:27.928560+0800 KVO_Demo[3830:156153] SEL:setName: -- IMP:0x108bbede0
2020-01-22 19:14:27.928622+0800 KVO_Demo[3830:156153] [self.person class] : Person
2020-01-22 19:14:27.928841+0800 KVO_Demo[3830:156153] ---- object_getClass(self.person) : NSKVONotifying_Person ----
2020-01-22 19:14:27.928902+0800 KVO_Demo[3830:156153] SEL:setName: -- IMP:0x108f19d1a
2020-01-22 19:14:27.928957+0800 KVO_Demo[3830:156153] SEL:class -- IMP:0x108f1874e
2020-01-22 19:14:27.929008+0800 KVO_Demo[3830:156153] SEL:dealloc -- IMP:0x108f184f2
2020-01-22 19:14:27.929065+0800 KVO_Demo[3830:156153] SEL:_isKVOA -- IMP:0x108f184ea
2020-01-22 19:14:27.929116+0800 KVO_Demo[3830:156153] [self.person class] : Person

从打印可以看出两次的打印都是Person

结论:

KVO 底层实现:首先KVO需要创建一个子类(NSKVONotyfing_Person),这个子类是继承于被观察对象的,这个子类需要重写属性的setter方法,这个时候,外界在调用setter方法的时候,调用的是子类重写的setter方法。就是让外界的person对象的isa指针指向这个子类。

简单自定义KVO

// NSObject+NHKVO.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (NHKVO)

- (void)nh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)nh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

NS_ASSUME_NONNULL_END

// NSObject+NHKVO.m
#import "NSObject+NHKVO.h"
#import <objc/message.h>

static NSString *const kNHKVOPrefix = @"NHKVONotifying_";
static NSString *const kNHKVOAssiociateKey = @"kNHKVO_AssiociateKey";

@implementation NSObject (KVO)

- (void)nh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    // 验证是否存在setter方法
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 动态创建子类
    Class class = [self createChildClassWithKeyPath:keyPath];
    // 重定向isa,指向新建的动态子类
    object_setClass(self, class);
    // 保存观察者,当观察的属性发生改变时,向这个对象发送消息
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kNHKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)nh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    // 指回给父类
    Class superClass = [self class];
    object_setClass(self, superClass);
}
// class IMP
Class nh_class(SEL id,IMP _cmd){
    return nil;
}

static void nh_setter(id self,SEL _cmd,id newValue){
    // 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
    
    // 既然观察到了,下一步不就是回调 -- 让我们的观察者调用
    // - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    // 1: 拿到观察者
    id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kNHKVOAssiociateKey));
    
    // 2: 消息发送给观察者
    SEL observerSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    objc_msgSend(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
    
}

#pragma mark - 动态创建子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kNHKVOPrefix,NSStringFromClass([self class])];
    Class newClass = NSClassFromString(newClassName);
    if (newClass) return newClass;
    // 如果内存中没有,则动态生成
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 注册类
    objc_registerClassPair(newClass);
    // 添加class方法
    SEL classSEL = NSSelectorFromString(@"class");
    // 从父类中获取class方法
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    // 获取方法类型
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)nh_class, classTypes);
    // 添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)nh_setter, setterTypes);
    return newClass;
}
#pragma mark - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
    Class superClass = object_getClass(self);
    SEL setterSEL    = NSSelectorFromString(setterForGetter(keyPath));
    Method method    = class_getInstanceMethod(superClass, setterSEL);
    if (!method) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"没有找到%@的setter方法",keyPath] userInfo:nil];
    }
}

#pragma mark - 从get方法获取set方法的名称 key ===>>> setKey:
static NSString *setterForGetter(NSString *getter){
    
    if (getter.length <= 0) { return nil;}
    
    NSString *firstString = [[getter substringToIndex:1] uppercaseString];
    NSString *leaveString = [getter substringFromIndex:1];
    
    return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}

#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
    
    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;
    }
    
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getter = [setter substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

@end

KVO使用

自动观察和手动观察

//如果需要自定义,需要重新此方法,默认返回YES
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;

//在set方法中手动调用,变化类型只是针对NSKeyValueChangeSetting
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

属性依赖

第一种、
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key NS_AVAILABLE(10_5, 2_0);
说明:
1、返回目标属性依赖属性的KeyPath的Set。当对象注册后,KVO自动监测该对象所有的KeyPaths。
2、其默认实现从对象所属类的方法列表中匹配方法:+keyPathsForValuesAffecting<Key>(<Key>为属性名,比如Name),如果存在执行并返回结果;如果不存在向底层寻找已经废弃的方法+setKeys:triggerChangeNotificationsForDependentKey:
3、可以用来替换手动调用-willChangeValueForKey:/-didChangeValueForKey:来实现属性依赖的解决方案
4、不能在已有类的Category中使用,在Category禁止重写此方法,可以使用+keyPathsForValuesAffecting<Key>来实现。

第二种、
或者重写此格式+keyPathsForValuesAffecting<Key>(<Key>为属性名,比如Name)的方法名

观察数据类型

// 这种添加值的方法监听不到
[self.person.dateArray addObject:@"hello"];
// 需要使用KVC获取值出来才能监听到变化
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"hello"];

参考文章

Key-Value Observing (键值监测)