IOS - KVO原理分析

本文首发于 个人博客

KVO一直是IOS面试中的重点,下面的面试题你碰到过吗?

  • KVO的底层是如何实现的?
  • addObserver:forKeyPath:options:context:context有什么用?
  • 直接修改成员变量会触发KVO吗?
  • 我们知道KVC会修改成员变量,那么它会触发KVO吗?
  • 如何监听可变数组的改变?

看到上述问题,你有答案了吗?如果你有疑惑,带着疑问我们开启一段KVO的探索之旅。

KVO全称Key-Value Observing,是苹果提供的一套事件通知机制,允许一个对象在其他对象的指定属性发生更改时得到通知的机制。

KVO初探

大家都了解KVO的基本使用方法,无非就是添加观察者、接收通知和移除观察者,下面我们通过一个简单的Demo来了解一下具体的实现。

#import "ViewController.h"
#import "SSBoy.h"
#import "SSGirl.h"
#import <objc/runtime.h>

@interface ViewController ()
@property (nonatomic, strong) SSBoy *boy;
@property (nonatomic, strong) SSGirl *girl;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.boy = [SSBoy new];
    self.girl = [SSGirl new];
    // 添加观察者
    [self.boy addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    [self.boy addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
    [self.girl addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 修改相应的值
    self.boy.name = @"sanliangsan";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    // 接收通知回调
    if ([object isEqual:self.boy]) {
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"boy change");
        }
    } else {
        NSLog(@"girl change");
    }
}
@end

上述代码清晰标注了KVO的简单实现,不过这份简单代码有一些问题哦,那么问题在哪呢?

boygirl同时观察了相同的name属性,我们的observeValueForKeyPath方法的接收中多层的嵌套判断比较复杂,而且还容易出错,这就引出了上述关于context的面试题,我们引用一段官方文档:

You may specify NULL and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons. A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.

我们可以指定NULL作为context,但是这样会因为一些不同的原因导致对象的父类也同时会观察相同的属性key,使用context可以更安全以及更具有扩展性。同时也告知了我们context如果为空应该是用NULL 而非nil

Context具体如何使用呢?

// 定义context
static void *BoyNameContext = &BoyNameContext;
// 添加观察者
[self.boy addObserver:self forKeyPath:@"name"options:NSKeyValueObservingOptionNew context:BoyNameContext];
// 接收通知回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == BoyNameContext) {
        // do....
    }
}

Context你会用了吗?聪明的你可能还发现了最上面的简单例子还有一个致命的问题!!!


正是:我们没有对相应观察者进行移除,在我们的观察者释放的时候我们要移除相应观察。

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

如果观察的对象是一个单例,而他在几个不同的场景都有观察同样的属性,那么在某个场景消失的时候别的地方触发属性修改就会导致单例去寻找已经释放的对象,就是野指针的情况。具体的实现还请各位自己去测试。到此KVO的简单实现你会了吗?

接下来我们看看KVO底层到底是如何实现的。

KVO底层原理

Automatic key-value observing is implemented using a technique called isa-swizzling. 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.

动态子类

KVO底层的实现是运用了一项 isa-swizzling 的技术,当我们添加观察者的时候,系统动态的给我们的对象创建了一个子类,将对象的isa指向了动态子类,而KVO的所有实现都是通过这个动态子类的,添加一个动态子类让类的职责更单一具体,而且让我们的KVO透明化,创建动态子类的过程我们是无法感知的,同时我们也知道了获取一个类不能通过isa的指向而是要看class的方法返回。多说无益,我们验证一下:

image

还是同样的代码,我们看到添加观察者之前,isa指向SSBoy,但是添加观察者之后就指向一个叫NSKVONotifying_SSBoy的类,这正好验证了上述文档所说。

成员变量和属性

KVO到底研究的是什么?

属性监听.jpg

说到属性,我们无不和实例变量牵扯到一起,他们之间的区别就是是否有setter方法,属性的修改我们在最初已经验证过了,现在我们看看修改实例变量是否会触发KVO

// 实例变量
@interface SSBoy : NSObject
{
    @public
    NSString *nickName;
}

 // 添加观察者
[self.boy addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:BoyNameContext];

// 修改实例变量
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 修改相应的值
    self.boy->nickName = @"sanliangsan";
}
// 没有响应
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    // 接收通知回调
    if (context == BoyNameContext) {

    }
}

最终我们的结果是不会触发,那么为什么成员变量的修改不会触发KVO,动态子类究竟都干了啥?

动态子类做了啥?

了解一个类我们无非是从属性,成员变量,方法等去研究,这里我们从方法入手,其他的大家可以下去一一验证。

image

我们去打印原类和动态子类的所有方法作以对比。

  • NSKVONotifying_SSBoy中为啥没有setAge

    因为没有针对age添加观察者,这也证明了KVO的动态中间子类是通过实现setter方法去实现的。

  • NSKVONotifying_SSBoy中为啥有class方法?

    重写class方法,因为class指向类本身,【伪装】为了让这一层更透明,苹 果重写class方法重新指向SSBoy,让上层对动态子类的生成没有感知,透明化&隐私化

  • dealloc方法为了啥?

    最初我们已经了解了,在添加观察者的时候会动态生成子类,而且对 象的isa会指向动态子类,当动态子类调用dealloc的时候,isa当然会重新指向回原类。

KVO与KVC

之前的一篇文章 KVC原理与自定义 有讲述KVC底层是如何一步一步实现修改对象的属性的,那么问题来了,KVC会触发KVO吗,仔细阅读KVO官方文档我们看到一段话:

NSObject provides a basic implementation of automatic ke y-value change notification. Automatic key-value change no tification informs observers of changes made using key-val ue compliant accessors, as well as the key-value coding me thods. Automatic notification is also supported by the col lection proxy objects returned by, for example, mutableAr rayValueForKey:

大概意思就是NSObject提供了自动key-value观察的实现,而且通过setter方法和key-value coding方法是一样的,换言之:key-value coding也能实现自动KVO,同时文档还给出相关能触发KVO的实例:

Examples of method calls that cause KVO change notifications to be emitted.

// Call the accessor method.
[account setName:@"Savings"];

 // Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];

// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];

 // Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

通过以上我们得知:KVC是能自动实现KVO的,而且可以验证不论是否有某属性都会自动通知到观察者。

KVO与可变数组

我们在某些情况下想对一个数组进行观察,添加、删除,修改等等,但是实际测试发现,普通的方法调用并不会触发KVO,其原因很简单,利用我们上述的原理就得以解释:我们对数组的各种添加、删除、修改并不会调用setter方法,由于KVC会触发KVO我们在KVC里边找到相关的方法得以实现:

[[_arrayModel mutableArrayValueForKeyPath:@"dataArray"] addObject:XXX];
[[_arrayModel mutableArrayValueForKeyPath:@"dataArray"] removeObject:XXX];

至此,我们对KVO的简单使用以及原理分析已经完结,那些面试题的答案你都知晓了吗?实际的使用过程中我们还会碰到更多的问题,期待你的交流和沟通。

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

推荐阅读更多精彩内容