[Objective-C]自实现KVO中的坑
[Objective-C]自实现KVO中的坑
Frida芥末 发表于11个月前
[Objective-C]自实现KVO中的坑
  • 发表于 11个月前
  • 阅读 54
  • 收藏 1
  • 点赞 0
  • 评论 0

标题:腾讯云 新注册用户域名抢购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

欢迎大家拍砖。

标签: Objective-C KVO
共有 人打赏支持
粉丝 0
博文 26
码字总数 12596
×
Frida芥末
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: