Runtime(4)--KVO的底层原理

KVO的常见问题

  • iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

1.利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
2.当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
3.这个函数会去调用重写的setter方法。
setter方法内部实现---
willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)

  • 如何手动触发KVO?

KVO一般都是自动就调用了。打点或者setValue调用。如果不这样操作也想调用kvo的监听,比如直接去修改成员变量(_属性)的值。就可以:
手动调用willChangeValueForKey:和didChangeValueForKey:

- (void)updateName:(NSString *)name {
    [self willChangeVauleForKey:@"name"];
    _name = name;
    [self didChangeVauleForKey:@"name"];
}

KVO的自动调用也是可以控制的,重写如下方法:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"name"]) {
        automatic = NO;//关闭了name属性的kvo监听的自动调用。手动如上操作还是可以调起kvo的监听。
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}
  • 直接修改成员变量的值会触发KVO么?

显而易见,不做特殊处理的话,不会触发kvo。

KVO的实现机制

那么,KVO背后是如何实现的呢?

主要说了两件事:

1.KVO是基于isa-swizzling技术实现的。isa-swizzling会将被观察对象的isa指针进行替换。
2.因为在实现KVO时,系统会替换掉被观察对象的isa指针,因此,不要使用isa指针来判断类的关系,而应该使用class方法。

我们准备了下面的实验代码,来窥探一下KVO的实现机制,是不是如我们所说的那样。

@interface Student : NSObject
@property(nonatomic, strong) NSString *name;
@property(nonatomic, strong) NSMutableArray *friends;
@end

@implementation Student
- (void)showObjectInfo {
    NSLog(@"Object instance address is %p, Object isa content is %p", self, *((void **)(__bridge void *)self));
}

@end

我们在Student类中定义了方法- (void)showObjectInfo,主要是用来打印Student实例的地址,以及Student 的isa指针中的内容。这可以用来研究系统是如何做isa-swizzling操作的。

然后准备下面的方法,来打印类的方法列表:

static NSArray * ClassMethodNames(Class c)
{
    NSMutableArray * array = [NSMutableArray array];
    unsigned int methodCount = 0;
    Method * methodList = class_copyMethodList(c, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++) {
        [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
    }
    
    free(methodList);
    return array;
}

运行如下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    Student *std = [Student new];
    // 1. 初始值
    std.name = @"Tom";
    NSLog(@"std->isa:%@", object_getClass(std));
    NSLog(@"std class:%@", [std class]);
    NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
    [std showObjectInfo];
    
    // 2. 添加KVO
    [std addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [std addObserver:self forKeyPath:@"friends" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    NSLog(@"std->isa:%@", object_getClass(std));
    NSLog(@"std class:%@", [std class]);
    NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
    [std showObjectInfo];
    std.name = @"Jack";

    // 3. 移除KVO
    [std removeObserver:self forKeyPath:@"name"];
    [std removeObserver:self forKeyPath:@"friends"];
    NSLog(@"std->isa:%@", object_getClass(std));
    NSLog(@"std class:%@", [std class]);
    NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
    [std showObjectInfo];
}

运行可以看到输出

1.没添加kvo之前的初始值:
std->isa:Student
std class:Student
ClassMethodNames:(
showObjectInfo,
"setFriends:",
friends,
".cxx_destruct",
"setName:",
name
)
Object address is 0x28194fe80, Object isa content is 0x1a1008090cd

2.添加kvo之后的打印:
std->isa:NSKVONotifying_Student
std class:Student
ClassMethodNames:(
"setFriends:",
"setName:",
class,
dealloc,
"_isKVOA"
)
Object address is 0x28194fe80, Object isa content is 0x1a282b5bf05

3.移除kvo之后的打印
std->isa:Student
std class:Student
ClassMethodNames:(
showObjectInfo,
"setFriends:",
friends,
".cxx_destruct",
"setName:",
name
)
Object address is 0x28194fe80, Object isa content is 0x1a1008090cd

通过观察添加KVO前、添加KVO后,移除KVO后这三个实际的Object地址信息可以知道,Object的地址并没有改变,但是其isa指针中的内容,却经历了如下变化:0x1a1008090cd->0x1a282b5bf05->0x1a1008090cd
对应的,通过object_getClass(std)方法来输出std的类型是:Student->NSKVONotifying_Student->Student

这就是所谓的isa-swizzling,当KVO时,系统会将被观察对象的isa指针内容做替换,让其指向新的类NSKVONotifying_Student,而在移除KVO后,系统又会将isa指针内容还原。

那么,NSKVONotifying_Student这个类又是什么样的呢?
通过打印其方法列表,可以知道,NSKVONotifing_Stdent定义或重写了如下方法:

ClassMethodNames:(
    "setFriends:",
    "setName:",
    class,
    dealloc,
    "_isKVOA"
)

可以看到,系统新生成的类重写了我们KVO的属性FriendsNameset方法

同时,还重写了class方法。通过runtime的源码可以知道,class方法:

- (Class)class {
    return object_getClass(self);
}

+ (Class)class {
    return self;
}

而在object_getClass方法中,会输出实例的isa指向的类:

{
    if (obj) return obj->getIsa();
    else return Nil;
}

按说[std class]object_getClass(std)的输出应该一致,但是系统会在KVO的时候,悄悄改写实例的class方法。这也就是为什么,当使用[std class]方法打印实例的类时,会输出Student而不是实际的NSKVONotifing_Student

然后系统还重写了dealloc方法,估计是为了在实例销毁时,做一些检查及清理工作。

最后,添加了_isKVOA方法,这估计是系统为了识别是KVO类而添加的。

这里,细心的同学会发现,在KVO之前Student的方法列表里面是包含属性的get方法showObjectInfo方法以及.cxx_destruct这些方法的。而当系统将Student替换为NSKVONotifing_Student后,这些方法那里去了呢?如果这些方法没有在NSKVONotifing_Student再实现一遍的话,那当KVO后,我们再调用属性的get方法showObjectInfo方法岂不是会crash?

但平日的编程实践告诉我们,并不会crash。那这些方法都去那里了呢?让我们来看一下NSKVONotifing_Student的父类是什么:

// 2. 添加KVO
...
Class objectRuntimeClass = object_getClass(std);
Class superClass = class_getSuperclass(objectRuntimeClass);
NSLog(@"super class is %@", superClass);

输出为:super class is Student
哈哈,很有意思吧,原来NSKVONotifing_Student的父类竟然是Student。那根据OC的消息实现机制,当在NSKVONotifing_Student中没有找到方法实现时,会自动到其父类Student中寻找相应的实现。因此,在NSKVONotifing_Student中,仅仅需要定义或重写KVO相关的方法即可,至于Student中定义的其他方法,则会在消息机制中在被自动找到。

  • 以上,便是KVO的isa-swizzling技术的大体实现流程。

1.当类实例被KVO后,系统会替换实例的isa指针内容。让其指向NSKVONotifing_XX类型的新类。
2.在NSKVONotifing_XX类中,会:重写KVO属性set方法,支持KVO。重写class方法,来伪装自己仍然是XX类。添加_isKVOA方法,来说明自己是一个KVO类。重写dealloc方法,让实例下析构时,好做一些检查和清理工作
3.为了让用户在KVO isa-swizzling后,仍然能够调用原始XX类中的方法,系统还会将NSKVONotifing_XX类设置为原始XX类的子类
4.当移除KVO后,系统会将isa指针中的内容复原。

自己写一个KVO

既然知道了KVO背后的实现原理,我们能不能利用runtime方法,模拟实现一下KVO呢?
当然可以,下来看下效果:

#import "ViewController.h"
#import "NSObject+KVOBlock.h"
#import <objc/runtime.h>
@implementation Student
@end
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Student *std = [Student new];
    // 直接用block回调来接受 KVO
    [std sw_addObserver:self forKeyPath:@"name" callback:^(id  _Nonnull observedObject, NSString * _Nonnull observedKeyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
        NSLog(@"old value is %@, new vaule is %@", oldValue, newValue);
    }];
    
    std.name = @"Hello";
    std.name = @"Lilhy";
    NSLog(@"class is %@, object_class is %@", [std class], object_getClass(std));
    [std sw_removeObserver:self forKeyPath:@"name"];
    NSLog(@"class is %@, object_class is %@", [std class], object_getClass(std));
    
}
@end

为了模拟的和系统KVO实现类似,我们也改写了class方法,在KVO移除前后,打印std的类信息为:
class is Student, object_class is sw_KVONotifing_Student
// 移除KVO后
class is Student, object_class is Student

在这里我手动实现了KVO,并通过Block的方式来接受KVO的回调信息。接下来我们就一步步的分析是如何做到的。我们应该重点观察所使用到的runtime方法。

首先,我们新建一个NSObject的分类NSObject (KVOBlock),并声明如下方法:

typedef void(^sw_KVOObserverBlock)(id observedObject, NSString *observedKeyPath, id oldValue, id newValue);

@interface NSObject (KVOBlock)
- (void)sw_addObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
              callback:(sw_KVOObserverBlock)callback;

- (void)sw_removeObserver:(NSObject *)observer
               forKeyPath:(NSString *)keyPath;

@end

在关键的sw_addObserver:forKeyPath:callback:中,是这么实现的:

static void *const sw_KVOObserverAssociatedKey = (void *)&sw_KVOObserverAssociatedKey;
static NSString *sw_KVOClassPrefix = @"sw_KVONotifing_";

- (void)sw_addObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
              callback:(sw_KVOObserverBlock)callback {
    // 1. 通过keyPath获取当前类对应的setter方法,如果获取不到,说明setter 方法即不存在与KVO类,也不存在与原始类,这总情况正常情况下是不会发生的,触发Exception
    NSString *setterString = sw_setterByGetter(keyPath);
    SEL setterSEL = NSSelectorFromString(setterString);
    Method method = class_getInstanceMethod(object_getClass(self), setterSEL);
    
    if (method) {
        // 2. 查看当前实例对应的类是否是KVO类,如果不是,则生成对应的KVO类,并设置当前实例对应的class是KVO类
        Class objectClass = object_getClass(self);
        NSString *objectClassName = NSStringFromClass(objectClass);
        if (![objectClassName hasPrefix:sw_KVOClassPrefix]) {
            Class kvoClass = [self makeKvoClassWithOriginalClassName:objectClassName]; // 为原始类创建KVO类
            object_setClass(self, kvoClass); // 将当前实例的类设置为KVO类
        }
        
        // 3. 在KVO类中查找是否重写过keyPath 对应的setter方法,如果没有,则添加setter方法到KVO类中
        // 注意,此时object_getClass(self)获取到的class应该是KVO class
        if (![self hasMethodWithMethodName:setterString]) {
            class_addMethod(object_getClass(self), NSSelectorFromString(setterString), (IMP)sw_kvoSetter, method_getTypeEncoding(method));
        }
        
        // 4. 注册Observer
        NSMutableArray<SWKVOObserverItem *> *observerArray = objc_getAssociatedObject(self, sw_KVOObserverAssociatedKey);
        if (observerArray == nil) {
            observerArray = [NSMutableArray new];
            objc_setAssociatedObject(self, sw_KVOObserverAssociatedKey, observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        SWKVOObserverItem *item = [SWKVOObserverItem new];
        item.keyPath = keyPath;
        item.observer = observer;
        item.callback = callback;
        [observerArray addObject:item];
        
        
    }else {  
        NSString *exceptionReason = [NSString stringWithFormat:@"%@ Class %@ setter SEL not found.", NSStringFromClass([self class]), keyPath];
        NSException *exception = [NSException exceptionWithName:@"NotExistKeyExceptionName" reason:exceptionReason userInfo:nil];
        [exception raise];
    }
}

上面的函数重点是:

1.调用makeKvoClassWithOriginalClassName方法来生成原始类对应的KVO类
2.利用class_addMethod方法,为KVO类添加改写的setter实现
完成了上面两点,一个手工的KVO实现基本就完成了。另一个需要注意的是,如何存储observer。在这里是通过一个MutableArray数组,当做Associated object来存储到类实例中的。

可以看出来,这里的重点在于如何创建原始类对应的KVO类

- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClassName {
    // 1. 检查KVO类是否已经存在, 如果存在,直接返回
    NSString *kvoClassName = [NSString stringWithFormat:@"%@%@", sw_KVOClassPrefix, originalClassName];
    Class kvoClass = objc_getClass(kvoClassName.UTF8String);
    if (kvoClass) {
        return kvoClass;
    }
    
    // 2. 创建KVO类,并将原始class设置为KVO类的super class
    kvoClass = objc_allocateClassPair(object_getClass(self), kvoClassName.UTF8String, 0);
    objc_registerClassPair(kvoClass);
    
    // 3. 重写KVO类的class方法,使其指向我们自定义的IMP,实现KVO class的‘伪装’
    Method classMethod = class_getInstanceMethod(object_getClass(self), @selector(class));
    const char* types = method_getTypeEncoding(classMethod);
    class_addMethod(kvoClass, @selector(class), (IMP)sw_class, types);
    return kvoClass;
}

其实实现也不难,调用了runtime的方法

1.objc_allocateClassPair(object_getClass(self), kvoClassName.UTF8String, 0) 动态生成新的KVO类,并设置KVO类的super class是原始类
2.注册KVO类 : objc_registerClassPair(kvoClass)
3.为了实现KVO伪装成原始类,还为KVO类添加了我们自己重写的class方法

    Method classMethod = class_getInstanceMethod(object_getClass(self), @selector(class));
    const char* types = method_getTypeEncoding(classMethod);
    class_addMethod(kvoClass, @selector(class), (IMP)sw_class, types);

// 自定义的class方法实现
static Class sw_class(id self, SEL selector) {
    return class_getSuperclass(object_getClass(self));  // 因为我们将原始类设置为了KVO类的super class,所以直接返回KVO类的super class即可得到原始类Class
}

那么当我们需要移除Observer时,需要调用sw_removeObserver:forKeyPath: 方法:

- (void)sw_removeObserver:(NSObject *)observer
               forKeyPath:(NSString *)keyPath {
    NSMutableArray<SWKVOObserverItem *> *observerArray = objc_getAssociatedObject(self, sw_KVOObserverAssociatedKey);
    [observerArray enumerateObjectsUsingBlock:^(SWKVOObserverItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj.observer == observer && [obj.keyPath isEqualToString:keyPath]) {
            [observerArray removeObject:obj];
        }
    }];
    
    if (observerArray.count == 0) { // 如果已经没有了observer,则把isa复原,销毁临时的KVO类
        Class originalClass = [self class];
        Class kvoClass = object_getClass(self);
        object_setClass(self, originalClass);
        objc_disposeClassPair(kvoClass);
    }   
}

注意,这里当Observer数组为空时,我们会将当前实例的所属类复原成原始类,并dispose掉生成的KVO类。
原码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。