文档章节

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

Zifirery
 Zifirery
发布于 2017/02/24 17:32
字数 1778
阅读 82
收藏 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
博文 30
码字总数 16974
作品 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
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

没有更多内容

加载失败,请刷新页面

加载更多

不可不说的Java“锁”事

前言 Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8)、使用场景进行举例,为读者介绍主流锁的知识点...

美团技术团队
1分钟前
0
0
ali oss util demo

package com.example.demo;import com.aliyun.oss.OSSClient;import com.aliyun.oss.common.utils.BinaryUtil;import com.aliyun.oss.model.*;import org.slf4j.Logger;import o......

经常把天聊死的胖子
2分钟前
0
0
Windows系统中eclipse修改字体为Courier New

背景:在eclipse修改字体时没有找到Courier New字体; 解决: 1.在计算机地址栏上输入“C:\Windows\Fonts”路径,回车打开Win10字体文件夹。查看是否有Courier New字体;如下图: 2.如果有该...

anlve
3分钟前
0
0
使用hexo做博客网站

hexo有什么用? hexo 可以把md文件生成html静态网页。 hexo官网:https://hexo.io/zh-cn/ 本地安装hexo。 npm install -g hexo-cli#生成blog(名字任意)文件夹,并且在这个文件夹里面初始化...

王坤charlie
3分钟前
0
0
RabbitMQ+PHP 教程四(Routing)用yii2测试通过

开始 在本教程中,我们将为它添加一个特性——我们将只可能订阅消息的一个子集。例如,我们只能够将关键错误消息直接指向日志文件(以节省磁盘空间),同时仍然能够打印控制台上的所有日志消...

hansonwong
7分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部