KVO实现原理及自己实现KVO

前言

Key-Value-Observer,它来源于观察者模式, 其基本思想(copy于某度)是一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。

本质

  • KVO 是 Objective-C 对观察者设计模式的一种实现,另外一种是:通知机制(notification)
  • KVO提供一种机制,指定一个被观察对象(例如A类),当对象某个属性(例如A中的字符串name)发生更改时,对象会获得通知,并作出相应处理

在MVC设计架构下的项目,KVO机制很适合实现mode模型和controller之间的通讯。
例如:代码中,在模型类A创建属性数据,在控制器中创建观察者,一旦属性数据发生改变就收到观察者收到通知,通过KVO再在控制器使用回调方法处理实现视图B的更新;(本文中的应用就是这样的例子.)

实现原理

KVO在Apple中的API文档如下:
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 …
KVO 的实现依赖于 Objective-C 强大的 Runtime【 ,从以上Apple 的文档可以看出苹果对于KVO机制的实现是一笔带过,而具体的细节没有过多的描述,但是我们可以通过Runtime的所提供的方法去探索关于KVO机制的底层实现原理.

基本的原理:

当观察某对象A时,KVO机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性keyPath的setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

深入剖析:

Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为: NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。

  • NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;
    所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。
    因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。

  • 子类setter方法剖析:KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法:
    被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;
    之后observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
    KVO为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

-(void)setName:(NSString *)newName
{
    [self willChangeValueForKey:@"name"];    //KVO在调用存取方法之前总调用
    [super setValue:newName forKey:@"name"]; //调用父类的存取方法
    [self didChangeValueForKey:@"name"];     //KVO在调用存取方法之后总调用
}

示例验证

//Person类
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@end

//controller
Person *per = [[Person alloc]init];
//断点1
[per addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
//断点2
per.name = @"小明";
[per removeObserver:self forKeyPath:@"name"];
//断点3

运行项目,

  • 断点1位置:

    可以看到isa指向Person类,我们也可以使用lldb命令查看:
(lldb) po [per class]
Person
(lldb) po object_getClass(per)
Person
(lldb) 
  • 断点2位置:
(lldb) po [per class]
Person
(lldb) po object_getClass(per)
NSKVONotifying_Person
(lldb) 
  • 断点3位置:
(lldb) po [per class]
Person
(lldb) po object_getClass(per)
Person
(lldb)

上面的结果说明,在per对象被观察时,framework使用runtime动态创建了一个Person类的子类NSKVONotifying_Person,而且为了隐藏这个行为,NSKVONotifying_Person重写了- class方法返回之前的类,就好像什么也没发生过一样。但是使用object_getClass()时就暴露了,因为这个方法返回的是这个对象的isa指针,这个指针指向的一定是个这个对象的类对象
然后来偷窥一下这个动态类实现的方法,这里请出一个NSObject的扩展NSObject+DLIntrospection,它封装了打印一个类的方法、属性、协议等常用调试方法,一目了然。

@interface NSObject (DLIntrospection) 
+ (NSArray *)classes; 
+ (NSArray *)properties; 
+ (NSArray *)instanceVariables; 
+ (NSArray *)classMethods; 
+ (NSArray *)instanceMethods; 
 
+ (NSArray *)protocols; 
+ (NSDictionary *)descriptionForProtocol:(Protocol *)proto; 
 
+ (NSString *)parentClassHierarchy; 
@end

然后继续在刚才的断点处调试:

// 断点1 
(lldb) po [object_getClass(per) instanceMethods] 
<__NSArrayI 0x8e9aa00>( 
- (void)setName:(id)arg0 , 
- (void).cxx_destruct, 
- (id)name 
) 
// 断点 2 
(lldb) po [object_getClass(per) instanceMethods] 
<__NSArrayI 0x8d55870>( 
- (void)setName:(id)arg0 , 
- (class)class, 
- (void)dealloc, 
- (BOOL)_isKVOA 
) 
// 断点 3 
(lldb) po [object_getClass(per) instanceMethods] 
<__NSArrayI 0x8e9cff0>( 
- (void)setName:(id)arg0 , 
- (void).cxx_destruct, 
- (id)name 
) 

大概就是说arc下这个方法在所有dealloc调用完成后负责释放所有的变量,当然这个和KVO没啥关系了,回到正题。
从上面断点2的打印可以看出,动态类重写了4个方法:

  • - setName:最主要的重写方法,set值时调用通知函数
  • - class隐藏自己必备啊,返回原来类的class
  • - dealloc做清理犯罪现场工作
  • - _isKVOA这就是内部使用的标示了,判断这个类有没被KVO动态生成子类

接下来验证一下KVO重写set方法后是否调用了- willChangeValueForKey:和- didChangeValueForKey:
最直接的验证方法就是在Person类中重写这两个方法:

@implementation Person 
- (void)willChangeValueForKey:(NSString *)key { 
    NSLog(@"%@", NSStringFromSelector(_cmd)); 
    [super willChangeValueForKey:key]; 
} 
- (void)didChangeValueForKey:(NSString *)key { 
    NSLog(@"%@", NSStringFromSelector(_cmd)); 
    [super didChangeValueForKey:key]; 
} 
@end 

自己代码实现KVO

由于系统是自动实现的派生类NSKVONotifying_Person, 这儿我们自己手动创建一个派生类ALINKVONotifying_Person, 集成自Person. 同时给NSObject创建一个分类, 让每一个对象都拥有我们自定义的KVO特性.

//NSObject+KVO.h
#import <Foundation/Foundation.h>
@interface NSObject (KVO)
- (void)czc_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end

//NSObject+KVO.m
#import "NSObject+KVO.h"
#import "ALINKVONotifying_Person.h"
#import <objc/message.h>
NSString *const ObserverKey = @"ObserverKey";

@implementation NSObject (KVO)
- (void)czc_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
    
    // 把观察者保存到当前对象
    objc_setAssociatedObject(self, (__bridge const void *)(ObserverKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 修改对象isa指针
    object_setClass(self, [ALINKVONotifying_Person class]);
}
@end

//ALINKVONotifying_Person.m
#import "ALINKVONotifying_Person.h"
#import <objc/runtime.h>
extern NSString *const ObserverKey;
@implementation ALINKVONotifying_Person
- (void)setName:(NSString *)name{
    NSString *oldName = self.name;
     [super setName:name];
    // 获取观察者
    id obsetver = objc_getAssociatedObject(self, ObserverKey);
    NSDictionary<NSKeyValueChangeKey,id> *changeDict = oldName ? @{NSKeyValueChangeNewKey : name, NSKeyValueChangeOldKey : oldName} : @{NSKeyValueChangeNewKey : name};
    [obsetver observeValueForKeyPath:@"name" ofObject:self change:changeDict context:nil];
}
@end

此时我们调用自己定义的监听方法, 效果和系统的也是一样的

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

推荐阅读更多精彩内容

  • 一、概述 KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则其观察...
    DeerRun阅读 9,984评论 11 33
  • KVC 什么是 KVC KVC 是 Key-Value-Coding 的简称。 KVC 是一种可以直接通过字符串的...
    LeeJay阅读 2,187评论 6 41
  • 本篇会对KVO的实现进行探究,不涉及太多KVO的使用方法,但是会有一些使用时的思考。 一、使用上的疑问 1.key...
    奋拓达阅读 478评论 0 2
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 2,983评论 0 26
  • 作者:wangzz原文地址:http://blog.csdn.net/wzzvictory/article/det...
    反调唱唱阅读 1,102评论 0 5