文档章节

[Objective-C]自实现KVO中的坑

Zifirery
 Zifirery
发布于 2017/02/24 17:32
字数 1778
阅读 86
收藏 1

[Objective-C]自实现KVO中的坑

什么是KVO?

KVO(Key Value Observing, 键值观察)是Objective-C对观察者模式的实现,每次当被观察对象的某个属性值发生改变时,注册的观察者便能获得通知。 使用KVO很简单,分为三个基本步骤:

  1. 注册观察者,指定被观察对象的属性:
    其中,person即为被观察对象,它的name属性即为被观察的属性。

     [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];  
    
  2. 在观察者中实现以下回调方法:

     - (void)observeValueForKeyPath:(NSString *)keyPath  
                   ofObject:(id)object  
                     change:(NSDictionary *)change  
                    context:(voidvoid *)context  
     {  
         // use the context to make sure this is a change in the address,  
         // because we may also be observing other things 
    
     	NSString *name = [object valueForKey:@"name"]; 
     	NSLog(@"new name is: %@", name);  
     }  
    

    只要person对象中的name属性发生变化,系统会自动调用该方法。

  3. 最后,不要忘记在dealloc中移除观察者

     -(void)dealloc  
     {  
         // must stop observing everything before this object is  
         // deallocated, otherwise it will cause crashes  
         for(Person *p in m_observedPeople){  
             [p removeObserver:self forKeyPath:@"name"];  
         }  
    
         [m_observedPeople release];  
         m_observedPeople = nil;  
     }  
    

KVO的原理

想快速地了解OC中使用的某项技术,最快捷高效的莫过于查看Apple的官方文档。但是关于KVO的具体实现原理,Apple的文档介绍的真是Can't Be Simple Any More!

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.

从介绍可以看出,KVO的实现用了所谓的“isa-swizzling”的技术,但是具体是怎么实现的却不得而知。不过,如果用runtime提供的方法去深入探究,便可以窥探其详细的原理。得益于Mike Ash的文章,我们可以详细了解KVO实现的技术细节。

简单介绍一下KVO的实现原理:

当设置一个类为观察对象时,系统会动态地创建一个新的类,这个新的类继承自被观察对象的类,还重写了基类被观察属性的setter方法。派生类在被重写的setter方法中实现真正的通知机制。最后,系统将这个对象的isa指针指向这个新创建的派生类,这样,被观察对象就变成了新创建的派生类的实例。(注:runtime中,对象的isa指针指向该对象所属的类,类的isa指针指向该类的metaclass。有关OC的对象、类对象、元类对象metaclass object和isa指针)。同时,新的派生类还重写了dealloc方法(removeObserver)。

顺便提一下KVO是建立在runtime的基础之上。

为什么要自己封装KVO(KVO的优缺点)

不可否认,KVO的功能确实很强大,但是它的缺点也很明显:

  1. 过于简单的API

    KVO中只有通过重写-observeValueForKeyPath:ofObject:change:context方法来获取通知,该方法有诸多限制:不能使用自定义的selector,不能使用block,而且当父类也要监听对象时,往往要写一大坨代码。

  2. 父类和子类同时存在KVO时(监听同一个对象的同一个属性),很容易出现对同一个keyPath进行两次removeObserver操作,从而导致程序crash。要避免这个问题,就需要区分出KVO是self注册的,还是superClass注册的,我们可以在 -addObserver:forKeyPath:options:context:和-removeObserver:forKeyPath:context这两个方法中传入不同的context进行区分。

自己如何实现KVO

废话那么多进入正题,我们自己实现KVO,并封装block。

2种方式实现KVO的方式:

  • 完全重写KVO实现
  • 基于apple的API封装

基于apple的API封装

  1. 新建NSObject+KVO扩展 ,添加addObserver(observer, keyPath, block)方法
  2. ObserverInfo(observer, keyPath, block) 保存在数组NSObject->observerList
  3. [observed addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
  4. 在observer Class -> Observer 里面实现-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(voidvoid *)context 。拦截keyPath,并执行block
  5. 移除观察者对象。 在observer Class -> Observer(不一定非得是Observer) 重写 -(void)dealloc移除观察者对象。

重点 : 1. 保存block信息,2. 执行block
observed 和 observer 可以是同一个对象。

完全重写KVO实现

  1. 创建Observed的子类KVO_Observed

  2. 复制Observed的setter方法,并重写加入

     [self willChangeValueForKey:key];
     [super setter:newValue];
     [self didChangeValueForKey:key];
    
  3. 执行block

     重写-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(voidvoid *)context
     或者
     在刚刚重写的setter方法中调用block
    

代码:

KVO中的坑-数据类型包装

代码都放出来了,这一节不用看了

😁😁😁😁

这是大坑。首先感谢 Sindri的小巢详解苹果的黑魔法 - KVO 的奥秘
完全重写KVO实现: LXD_KeyValueObserve
这个代码也是他的。

  • 坑一: 我就问: 完全自己重写是不是很牛逼! 当我很开心的用的时候,就发现坑了。 研究一了发现,是基本数据类型或结构体都会出现BAD_ACCESS错误。 刚好我们可爱的OC又不支持方法重载,就必须实现多个set方法。对不能类型的属性,实现不同的方法。

    大体上分三类:

    • 对象 id
    • 基本数据类型 int short long float double char bool
    • 结构体
  • 坑二:

      typedef void (^LXD_ObservingHandler) (id observedObject, NSString * observedKey, id oldValue, id newValue);
    

    回调的 oldValue newValue类型都是id。
    所以对于基本数据类型和结构体要进行对象包装。基本数据类型->NSNumber 结构体->NSValue OC不支持方法重载!OC不支持方法重载!OC不支持方法重载!所以我们不能一个方法搞定一切。那就是写if-else吧。

if (![self hasSelector: setterSelector]) {
        //在类中添加方法和实现
        const char * types = method_getTypeEncoding(setterMethod);
        
        Class observedClass = object_getClass(self);
        objc_property_t property_t = class_getProperty(observedClass, key.UTF8String);
        
        NSString *attributes = [NSString stringWithCString:property_getAttributes(property_t) encoding:NSUTF8StringEncoding];
        
        IMP setterIMP;
        if ([attributes hasPrefix:@"T@"]) {
            setterIMP = (IMP)KVO_setter_id;
        }else if ([attributes hasPrefix:@"T{"]) {
            if ([attributes hasPrefix:@"T{CGPoint"]) {
                setterIMP = (IMP)KVO_setterValue_CGPoint;
            }
            else if ([attributes hasPrefix:@"T{CGRect"]) {
                setterIMP = (IMP)KVO_setterValue_CGRect;
            }
            else if ([attributes hasPrefix:@"T{CGVector"]) {
                setterIMP = (IMP)KVO_setterValue_CGVector;
            }
            else if ([attributes hasPrefix:@"T{CGSize"]) {
                setterIMP = (IMP)KVO_setterValue_CGSize;
            }
            else if ([attributes hasPrefix:@"T{CGAffineTransform"]) {
                setterIMP = (IMP)KVO_setterValue_CGAffineTransform;
            }
            else if ([attributes hasPrefix:@"T{UIEdgeInsets"]) {
                setterIMP = (IMP)KVO_setterValue_UIEdgeInsets;
            }
            else if ([attributes hasPrefix:@"T{UIOffset"]) {
                setterIMP = (IMP)KVO_setterValue_UIOffset;
            }
            else if ([attributes hasPrefix:@"T{_NSRange"]) {
                setterIMP = (IMP)KVO_setterValue_NSRange;
            }
            else if ([attributes hasPrefix:@"T{CATransform3D"]) {
                setterIMP = (IMP)KVO_setterValue_CATransform3D;
            }else {
                NSAssert(NO, @"Can't identify Struct");
            }
        }else {
            if ([attributes hasPrefix:@"Tc"]) {
                setterIMP = (IMP)KVO_setterNumber_char;
            }
            else if ([attributes hasPrefix:@"TC"]) {
                setterIMP = (IMP)KVO_setterNumber_UnsignedChar;
            }
            else if ([attributes hasPrefix:@"Ts"]) {
                setterIMP = (IMP)KVO_setterNumber_short;
            }
            else if ([attributes hasPrefix:@"TS"]) {
                setterIMP = (IMP)KVO_setterNumber_UnsignedShort;
            }
            else if ([attributes hasPrefix:@"Ti"]) {
                setterIMP = (IMP)KVO_setterNumber_int;
            }
            else if ([attributes hasPrefix:@"TI"]) {
                setterIMP = (IMP)KVO_setterNumber_UnsignedInt;
            }
            else if ([attributes hasPrefix:@"Tq"]) {
                setterIMP = (IMP)KVO_setterNumber_long;
            }
            else if ([attributes hasPrefix:@"TQ"]) {
                setterIMP = (IMP)KVO_setterNumber_UnsignedLong;
            }
            else if ([attributes hasPrefix:@"Tf"]) {
                setterIMP = (IMP)KVO_setterNumber_float;
            }
            else if ([attributes hasPrefix:@"Td"]) {
                setterIMP = (IMP)KVO_setterNumber_double;
            }
            else if ([attributes hasPrefix:@"TB"]) {
                setterIMP = (IMP)KVO_setterNumber_BOOL;
            }else{
                NSAssert(NO, @"Can't identify Basic data types");
            }
        }
        class_addMethod(observedClass, setterSelector, setterIMP, types);
    }

上面的问题,我已经发给作者issue了。我也提交了我的版本。 最后放上我的版本,兼容基本数据类型和结构体的自实现KVO

欢迎大家拍砖。

© 著作权归作者所有

共有 人打赏支持
Zifirery
粉丝 0
博文 33
码字总数 17670
作品 0
南京
iOS工程师
私信 提问
如何自己动手实现 KVO

本文是 Objective-C Runtime 系列文章的第三篇。如果你对 Objective-C Runtime 还不是很了解,可以先去看看前两篇文章: Objective-C Runtime Method Swizzling 和 AOP 实践 本篇会探究 KVO ...

zh_iOS
2016/08/22
302
0
Objective-C Runtime(四)isa swizzling

Runtime 4 isa swizzling Objective-C Runtime(一) 简介 对象、类的结构 消息传递(Messaging) Objective-C Runtime(二) 动态方法解析和转发 Objective-C Runtime(三) Method Swizzli...

liuyanhongwl
2018/03/27
0
0
详解Objective-C runtime

原文地址:http://blog.securemacprogramming.com/2013/12/by-your-cmd/ 感谢翻译小组成员wingpan热心翻译。本篇文章是我们每周推荐优秀国外的技术类文章的其中一篇。如果您有不错的原创或译...

Michael-W
2014/01/06
0
0
KVC 与 KVO 理解

KVC 与 KVO 是 Objective C 的关键概念,个人认为必须理解的东西,下面是实例讲解。 Key-Value Coding (KVC) KVC,即是指 NSKeyValueCoding,一个非正式的 Protocol,提供一种机制来间接访问...

Im刘亚芳
2014/12/04
0
0
教程1:Objective-C

Objective-C的教程已经看过了。 内容大概有:[Objective-C基础语法(if/else/switch/for...),关键字,运算符],[面向对象(封装/继承/多态)],[Foundation框架常用类],[内存管理],[协...

殷美洪
2013/03/11
0
0

没有更多内容

加载失败,请刷新页面

加载更多

如何在 Linux 系统查询机器最近重启时间

在你的 Linux 或类 UNIX 系统中,你是如何查询系统上次重新启动的日期和时间?怎样显示系统关机的日期和时间? last 命令不仅可以按照时间从近到远的顺序列出该会话的特定用户、终端和主机名...

来来来来来
今天
1
0
Redis协议是什么样的

前言 我们用过很多redis的客户端,有没有相过自己撸一个redis客户端? 其实很简单,基于socket,监听6379端口,解析数据就可以了。 redis协议 解析数据的过程主要依赖于redis的协议了。 我们...

春哥大魔王的博客
今天
3
0
乱入Linux界的我是如何学习的

欢迎来到建哥学Linux,咳!咳!咳!开个玩笑哈,我是一个IT男,IT界的入门选手,正在学习Linux。 在之前,一直想进军IT界,学习IT技术,但是苦于没有人指导,也不知道学什么,最开始我自己在...

linuxCool
今天
3
0
携程Apollo统一配置中心的搭建和使用(java)

一.Apollo配置中心介绍 1、What is Apollo 1.1 Apollo简介 Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到...

morpheusWB
今天
2
0
远程获得的有趣的linux命令

使用这些工具从远程了解天气、阅读资料等。 我们即将结束为期 24 天的 Linux 命令行玩具日历。希望你有一直在看,如果没有,请回到开始,从头看过来。你会发现 Linux 终端有很多游戏、消遣和...

Linux就该这么学
今天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部