美文网首页iOS原理KVOiOS
Objective-C runtime机制(10)——KVO的实

Objective-C runtime机制(10)——KVO的实

作者: 无忘无往 | 来源:发表于2019-02-22 10:30 被阅读23次

    使用KVO

    自动触发KVO

    在平日代码中,我们通过KVO来监视实例某个属性的变化。
    比如,我们要监视Student 的 age属性,可以这么做:

    @interface Student : NSObject
    @property(nonatomic, strong) NSString *name;
    @end
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        Student *std = [Student new];
        std.name = @"Tom";
        [std addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
     }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        ...
    }
    @end
    

    我们使用KVO需要遵循以下步骤:

    1. 调用addObserver:forKeyPath:options:context: 方法来注册观察者,观察者可以接收到KeyPath对应属性的修改通知
    2. 当观察的属性发生变化时,系统会在observeValueForKeyPath:ofObject:change:context:方法中回调观察者
    3. 当观察者不需要监听变化是,需要调用removeObserver:forKeyPath:KVO移除。需要注意的是,在观察者被释放前,必须要调用removeObserver:forKeyPath:将其移除,否则会crash。

    手动触发KVO

    当我们设置了观察者后,当被观察的keyPath对应的setter方法调用后,则会自动的触发KVO的回调函数。那么,有时候我们想要控制这种自动触发的机制,该怎么办呢?你可以重写如下方法:

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
        BOOL automatic = NO;
        if ([theKey isEqualToString:@"balance"]) {
            automatic = NO;
        }
        else {
            automatic = [super automaticallyNotifiesObserversForKey:theKey];
        }
        return automatic;
    }
    

    automaticallyNotifiesObserversForKey方法声明在NSObject的Category NSObject(NSKeyValueObservingCustomization)中。

    除了在setter方法中,有时候我们想主动触发一下KVO,该怎么办呢?
    那就需要使用

    willChangeValueForKey:
    didChangeValueForKey:
    

    来通知系统Key Value发生了改变。如:

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

    KVO实现机制

    那么,KVO背后是如何实现的呢?在苹果的官方文档上,有一个笼统的描述

    Automatic key-value observing is implemented using a technique called
    isa-swizzling.

    The isa pointer, as the name suggests, points to the object's class
    which maintains a dispatch table. This dispatch table essentially
    contains pointers to the methods the class implements, among other
    data.

    When an observer is registered for an attribute of an object the isa
    pointer of the observed object is modified, pointing to an
    intermediate class rather than at the true class. As a result the
    value of the isa pointer does not necessarily reflect the actual class
    of the instance.

    You should never rely on the isa pointer to determine class
    membership. Instead, you should use the class method to determine the
    class of an object instance.

    主要说了两件事:

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

    为什么要替换掉isa指针?文档中说的很清楚,因为isa指针会指向类实例对应的类的方法列表,而替换掉了isa指针,相当于替换掉了类的方法列表。

    那么为啥要替换类的方法列表呢?又是怎么替换的呢?文档到这里戛然而止,没有细说。

    下面,我们就用代码实验的方式,来窥探一下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. 初始值

    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的属性Friends和Name的set方法

    同时,还重写了class方法。通过runtime的源码可以知道,class方法实际是调用了object_getClass方法

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

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

    Class object_getClass(id obj)
    {
        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类

    完整的源码在这里

    KVO crash的避免

    总结

    参考

    KVO原理分析及使用进阶
    如何自己动手实现 KVO

    相关文章

      网友评论

        本文标题:Objective-C runtime机制(10)——KVO的实

        本文链接:https://www.haomeiwen.com/subject/dkpsyqtx.html