文档章节

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

Frida芥末
 Frida芥末
发布于 2017/02/24 17:32
字数 1778
阅读 63
收藏 1
点赞 0
评论 0

[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

欢迎大家拍砖。

© 著作权归作者所有

共有 人打赏支持
Frida芥末
粉丝 0
博文 27
码字总数 12810
作品 0
南京
iOS工程师
如何自己动手实现 KVO

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

zh_iOS ⋅ 2016/08/22 ⋅ 0

Objective-C Runtime(四)isa swizzling

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

liuyanhongwl ⋅ 03/27 ⋅ 0

ios技术面试题

1.Difference between shallow copy and deep copy? 浅复制 只拷贝地址 不拷贝地址指向的对象 深复制 拷贝地址 并且指向拷贝的新对象 2.What is advantage of categories? What is differenc...

AmoyAI ⋅ 2012/12/08 ⋅ 0

iOS KVO实现方式

KVO 也许是iOS中“最神奇”的部分了,因为你不需要在被观察对象中添加任何代码,就可以实现对被观察对象属性改变的通知。KVO究竟是怎么实现的? KVO是通过Objective-C的runtime来实现的。当...

董桉远 ⋅ 2013/11/27 ⋅ 0

详解Objective-C runtime

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

Michael-W ⋅ 2014/01/06 ⋅ 0

教程1:Objective-C

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

殷美洪 ⋅ 2013/03/11 ⋅ 0

KVC 与 KVO 理解

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

Im刘亚芳 ⋅ 2014/12/04 ⋅ 0

KVC/KVO 的使用及原理分析

KVC/KVO 概念 KVC : 即 Key-Value-Coding,用于键值编码。作为 cocoa 的一个标准化组成部分,它是基于 NSKeyValueCoding 非正式协议的机制。简单来说,就是直接通过 key 值对对象的属性进行...

满脸胡茬的小码农 ⋅ 2017/11/14 ⋅ 0

IOS中的KVO机制详解

ios开发有多种设计模式,其中有一种就叫做观察者模式,即Key Value Observing(简称KVO) KVO是Object -C中原声支持的一种机制. C、KVO 实现原理 当对一个对象添加观察者,被观察对象的属性值发生...

哪一种黑 ⋅ 2016/02/29 ⋅ 0

如何为一个实例动态替换方法

这个 Tip 来源于一道面试题,感觉很是考察知识变通的能力,对 KVO 深入了解的同学,应该很容易就可以答出来。这里抛砖引玉,简单聊聊这个 Tip 首先简单总结下 KVO 的大概原理 当你观察一个对...

Joy_xx ⋅ 2017/11/15 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

解决yum安装报错Protected multilib versions

使用yum安装报错Protected multilib versions原因是因为多个库不能共存,不过更新的话也并不行,但是可以在安装命令后面加上如下一段命令: --setopt=protected_multilib=false 案例: 比如需...

北岩 ⋅ 25分钟前 ⋅ 0

为什么要学习Typescript???

简单来说 目前的typescript就是未来的javascript 为什么?? 这要从ECMA-262标准的第4版说起 对了 我们说的ES5 其实是ECMAScript3.1这个替代性建议被扶正了而已... 那么 第4版标准是什么? 看看...

hang1989 ⋅ 29分钟前 ⋅ 0

linux安装ipfs

一、下载ipfs # cd /usr/local/ipfs/ # wget https://dist.ipfs.io/go-ipfs/v0.4.15/go-ipfs_v0.4.15_linux-amd64.tar.gz # tar -zxvf go-ipfs_v0.4.15_linux-amd64.tar.gz 二、安装ipfs # ......

八戒八戒八戒 ⋅ 35分钟前 ⋅ 0

jvm程序执行慢诊断手册

生产环境最多的几种事故之一就是程序执行慢,如果是web服务的话,表现就是响应时间长。本文分享,从业多年形成的排查守则。 诊断步骤 系统资源查看 首先是系统资源查看,而且必须是在第一步。...

xpbob ⋅ 35分钟前 ⋅ 0

YII2 advanced 高级版本项目搭建-添加API应用以及多应用

一、YII安裝 安裝yii可以用composer安裝,也可以在yii中文社区下载归档文件安装 composer安装就不介绍了,因为要安装composer,比较麻烦,当然安装了composer是最好的,以后安装yii的插件要用...

botkenni ⋅ 36分钟前 ⋅ 0

在jdk1.8的环境下模拟永久代内存溢出

相信不少小伙伴在看深入理解Java虚拟机的时候,作者给我们举例一个demo来发生PermGen space 1、通过List不断添加String.intern(); 2、通过设置对应的-XX:PermSize与-XX:MaxPermSize(更快看到...

虾几把写 ⋅ 今天 ⋅ 0

开发OpenDaylight组件的完整流程

在前面介绍学习了OpenDaylight的几个重要模块后,这里再来介绍下完整开发一个模块的过程。 OSGI的bundles提供被其他OSGI组件调用的服务。这个教程中展示的是Data Packet Service去解析数据包...

wangxuwei ⋅ 今天 ⋅ 0

Java序列化和反序列化

1、什么是序列化和反序列化 序列化:把对象转换为字节序列的过程。 反序列化:把字节序列恢复成对象的过程。 2、被序列化的类需要实现serializable接口,只是为了标注该对象是可以被序列化的...

IT-Mamba ⋅ 今天 ⋅ 0

流式构建原理

流式构建需要达到分钟级的数据更新频率,Kylin采用类似于Spark Streaming的做法,每隔数分钟进行一次微构建。这边的构建需要考虑到一个延迟因素,分布式网络存在延迟等因素,该时间段的数据有...

无精疯 ⋅ 今天 ⋅ 0

在maven项目工程编写solr代码,需要的依赖

solrJ <dependency> <groupId>org.apache.solr</groupId> <artifactId>solr-solrj</artifactId> <version>6.6.2</version> </dependency> <dependency> <groupId>org.apache.httpcomponents<......

爱运动的小乌龟 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部