KVO底层原理及Block方式回调实现

前言

  1. 本文不详细概述 KVO 的用法,只结合网上的资料说说对这种技术的底层实现原理。如需参考具体用法移步 KVO具体应用
  2. 本文探究底层技术参考来源最新官方开源代码objc4-723 需要对runtime有一定深度的了解参考Runtime 你为何如此之屌?
  3. 本文具体实现 KVO 功能通过Block方式回调,增加 KVO ds的使用体验。具体实现参考源码

概述

KVO (Key-Value Observing) 是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于 KVO 的实现机制,所以对属性才会发生作用,一般继承自 NSObject 的对象都默认支持 KVO

KVONSNotificationCenter 都是 iOS 中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO 是一对一的,而NSNotificationCenter是一对多的。KVO 对被监听对象无侵入性,不需要修改其内部代码即可实现监听。

KVO 的实现依赖于 OC 强大的 Runtime
KVOCocoa 提供的一种基于 KVC 的机制

实现原理

KVO 是通过 isa-swizzling 技术实现的(这句话是整个 KVO 实现的重点)。

  1. 当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类(子类),在这个派生类中重写基类(父类)中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制。

  2. 如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
    ,每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察监听,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法。

  3. 键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。

  4. KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类

代码解析

- (void)IS_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(ISObservingBlock)block
{
    //1.
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod(object_getClass(self), setterSelector);
    if (!setterMethod) {
        NSString *reason = [NSString stringWithFormat:@"Object %@ does not have a setter for key %@", self, key];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return;
    }
    
     //2.
    Class clazz = object_getClass(self);
    NSString *clazzName = NSStringFromClass(clazz);
    
    // if not an KVO class yet
    if (![clazzName hasPrefix:kISKVOClassPrefix]) {
        clazz = [self makeKvoClassWithOriginalClassName:clazzName];
        object_setClass(self, clazz);
    }
    
    //3.
    // add our kvo setter if this class (not superclasses) doesn't implement the setter?
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    }
    
    // 4.
    ISObservationInfo *info = [[ISObservationInfo alloc] initWithObserver:observer key:key block:block];
    NSMutableArray *observers = objc_getAssociatedObject(self, kISKVOAssociatedObservers);
    if (!observers) {
        observers = [NSMutableArray array];
    }
    [observers addObject:info];
    objc_setAssociatedObject(self,kISKVOAssociatedObservers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
  1. 通过Method判断是否有这个key对应的selector,如果没有则Crash。
  2. 判断当前类是否是KVO子类,如果不是则创建,并设置其isa指针。
  3. 如果没有实现,则添加Key对应的setter方法。
  4. 将调用对象添加到数组中。
static void kvo_setter(id self, SEL _cmd, id newValue)
{
    //1.
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
    
    if (!getterName) {
        NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return;
    }
    
    id oldValue = [self valueForKey:getterName];
    
    //2.
    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    // cast our pointer so the compiler won't complain
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
    // call super's setter, which is original class's setter method
    objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
    
    //3.
    // look up observers and call the blocks
    NSMutableArray *observers = objc_getAssociatedObject(self,kISKVOAssociatedObservers);
    for (ISObservationInfo *each in observers) {
        if ([each.key isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                each.block(self, getterName, oldValue, newValue);
            });
        }
    }
}

  1. 获取旧值。
  2. 创建super的结构体,并向super发送属性的消息。这一步不是必须的。系统kvo api没有这一步的实现。
  3. 遍历调用block。
- (void)IS_removeObserver:(NSObject *)observer forKey:(NSString *)key
{
    NSMutableArray* observers = objc_getAssociatedObject(self, kISKVOAssociatedObservers);
    
    ISObservationInfo *infoToRemove;
    for (ISObservationInfo* info in observers) {
        if (info.observer == observer && [info.key isEqual:key]) {
            infoToRemove = info;
            break;
        }
    }
    
    [observers removeObject:infoToRemove];
}

遍历观察者对象数组,移除指定观察者对象。

扩展问题

  1. [ARC下dealloc过程及.cxx_destruct的探究] (http://blog.sunnyxx.com/2014/04/02/objc_dig_arc_dealloc/)
    messageNoObserver: <Message: 0x60400000aeb0>
    NSObject class Message
    Runtime class Message
    implements methods <.cxx_destruct, text, setText:>

    message: <Message: 0x604000002f50>
    NSObject class Message
    Runtime class ISKVOClassPrefix_Message
    implements methods <setText:, class>
  1. KVO原理分析及使用进阶 测试代码 描述了通过系统API调用KVO前打印信息中有method Name = dealloc 对于这个方法怎么理解?
//使用kvo前
object address : 0x604000239340
object setName: IMP 0x10ea8defe object setAge: IMP 0x10ea94106
objectMethodClass : KVOObject, ObjectRuntimeClass : NSKVONotifying_KVOObject, superClass : KVOObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA

//使用kvo后
object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age

参考文章解读以及推荐阅读星级(🌟)——(🌟🌟🌟🌟🌟)

🌟🌟🌟🌟🌟KVO原理分析及使用进阶

1.KVO Cocoa Foundation API使用解析
2.isa指针理解(便于理解 isa-swizzling 技术)
3.详细的KVO缺点描述
4.手写KVO Block具体实现并阐述了使用手写kvo的注意事项
5.推荐使用手写KVO的使用方式 KVOController 提供该开源项目源码解析

🌟🌟🌟🌟 KVO具体应用

1.详细描述了KVO的使用
2.详细的原理介绍
3.特别突出的是对 NSNotificationCenter Delegate KVO进行了详细的横向对比
4.扩展了KVO相关问题
5.Github项目代码

🌟🌟🌟🌟KVC/KVO原理详解及编程指南

1.详细的代码示例
2.对KVC有特别详尽的使用和原理解析
3.提供结论涉及KVO/KVC的多种使用方式优缺点。

🌟🌟🌟🌟🌟iOS KVO crash 自修复技术实现与原理解析

KVO的安全使用方式,阅读难度偏大。

🌟🌟🌟🌟Runtime 你为何如此之屌?

从苹果开源代码的角度详细描述了runtime的理解和使用

🌟🌟探究KVO的底层实现原理

1.简单描述KVO/KVC实现原理
2.附有原理实现图和代码引用
3.引用的文章不错🌟🌟🌟如何自己动手实现 KVO

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

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,339评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,092评论 1 32
  • KVC KVC定义 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过K...
    暮年古稀ZC阅读 2,132评论 2 9
  • KVC KVC定义 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过K...
    jackyshan阅读 51,856评论 9 200
  • 一群鹅在四月的上海里嘎嘎叫 叫醒了香花桥路两旁梧桐树的春天 我不经意路过,慌乱中不敢多看一眼 我又再次走进冬季里 ...
    粮食和花圈阅读 205评论 1 5