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

使用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 文中的实验代码我放在了这个项目中。 以下内容是我通过整理[这篇博客] (http://yulingtianxia....
    茗涙阅读 904评论 0 6
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 724评论 0 2
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 791评论 0 4
  • 我的身体是我最大的财富,它如此精妙,能够思考,如果没有肉体,我的灵魂又何处安放,它由千千万万粒子组成,没有一丝误差...
    如你一般9阅读 113评论 0 1