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

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

什么是KVO?

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

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

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

    - (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属性发生变化,系统会自动调用该方法。github

  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!app

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.ide

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

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

当设置一个类为观察对象时,系统会动态地建立一个新的类,这个新的类继承自被观察对象的类,还重写了基类被观察属性的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

欢迎你们拍砖。

相关文章
相关标签/搜索