zoukankan      html  css  js  c++  java
  • 一句代码,更加优雅的调用KVO和通知

    来源:wazrx 

    链接:http://www.jianshu.com/p/70b2503d5fd1

    写在前面

    每次使用KVO和通知我就觉得是一件麻烦的事情,即便谈不上麻烦,也可说是不方便吧,对于KVO,你需要注册,然后实现监听方法,最后还要移除,通知当然也需要移除操作,这使得相关逻辑的代码过于分散,控制器搞得乱乱的,而且总有时候会忘记移除什么的,总之感觉不太好,所以我想如果能有方法添加一个KVO或者通知后能够省略后面移除或者实现监听方法步骤的话会多好,所以我就尝试写了一个分类,这个分类的目的在于尽可能简化KVO和通知的步骤,对于KVO,你只需要一句代码就可完成监听,无需自己手动移除,通知也差不多,接口如下:

    /**

    *  通过Block方式注册一个KVO,通过该方式注册的KVO无需手动移除,其会在被监听对象销毁的时候自动移除

    *

    *  @param keyPath 监听路径

    *  @param block   KVO回调block,obj为监听对象,oldVal为旧值,newVal为新值

    */

    - (void)xw_addObserverBlockForKeyPath:(NSString*)keyPath block:(void (^)(id obj, id oldVal, id newVal))block;

    /**

    *  通过block方式注册通知,通过该方式注册的通知无需手动移除,同样会自动移除

    *

    *  @param name  通知名

    *  @param block 通知的回调Block,notification为回调的通知对象

    */

    - (void)xw_addNotificationForName:(NSString *)name block:(void (^)(NSNotification *notification))block;

    使用也很简单咯,github地址如下:https://github.com/wazrx/XWEasyKVONotification.git

    你只需要导入NSObject+XWAdd这个分类,然后调用上面两个接口即可完成KVO和通知,事例代码如下

    //监听_objA的name属性

        [_objA xw_addObserverBlockForKeyPath:@"name" block:^(id obj, id oldVal, id newVal) {

            NSLog(@"kvo,修改name为%@", newVal);

        }];

        [self xw_addNotificationForName:@"XWTestNotificaton" block:^(NSNotification *notification) {

            NSLog(@"收到通知:%@", notification.userInfo);

        }];

    是不是非常简单,再也不用关心忘记移除导致的崩溃了,而且代码也集中,看着也更舒服了

    原理

    1、由于KVO和通知都差不多,原理部分通过KVO的接口的的实现原理进行说明,考虑到代码的统一我首先考虑到使用block,同时为了block能回调,我们需要一个内部的对象target的来实现KVO的代码,在监听到值改变的时候通过这个对象来回调block,同时一个target应该对应一个keyPath,并且可应该对应多个Block,因为我们可能对一个keyPath进行多处监听,这个类的具体代码大致如下:

    @interface _XWBlockTarget : NSObject

    /**添加一个KVOblock*/

    - (void)xw_addBlock:(void(^)(__weak id obj, id oldValue, id newValue))block;

    @end

    @implementation _XWBlockTarget{

        //保存所有的KVOblock

        NSMutableSet *_blockSet;

    }

    - (instancetype)init

    {

        self = [super init];

        if (self) {

            _blockSet = [NSMutableSet new];

        }

        return self;

    }

    - (void)xw_addBlock:(void(^)(__weak id obj, id oldValue, id newValue))block{

        [_blockSet addObject:[block copy]];

    }

    //KVO的真正实现

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{

        if (!_blockSet.count) return;

        BOOL prior = [[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue];

        //只接受值改变时的消息

        if (prior) return;

        NSKeyValueChange changeKind = [[change objectForKey:NSKeyValueChangeKindKey] integerValue];

        if (changeKind != NSKeyValueChangeSetting) return;

        id oldVal = [change objectForKey:NSKeyValueChangeOldKey];

        if (oldVal == [NSNull null]) oldVal = nil;

        id newVal = [change objectForKey:NSKeyValueChangeNewKey];

        if (newVal == [NSNull null]) newVal = nil;

        //当KVO触发,值改变的时候执行该target下的所有block

        [_blockSet enumerateObjectsUsingBlock:^(void (^block)(__weak id obj, id oldVal, id newVal), BOOL * _Nonnull stop) {

            block(object, oldVal, newVal);

        }];

    }

    @end

    2、实际进行KVO的监听的对象有了,我们就可以开始书写逻辑了,我们给每一个对象绑定一个targets的字典,每次调用该API注册KVO的就去判断有没有对应的keyPath下的target(target和keyPath一一对应),没有就创建,同时注册这个keyPath的KVO,有就把block加入这个target以便回调,具体代码如下:

    - (void)xw_addObserverBlockForKeyPath:(NSString*)keyPath block:(void (^)(id obj, id oldVal, id newVal))block {

        if (!keyPath || !block) return;

        //取出存有所有KVOTarget的字典

        NSMutableDictionary *allTargets = objc_getAssociatedObject(self, XWKVOBlockKey);

        if (!allTargets) {

            //没有则创建

            allTargets = [NSMutableDictionary new];

            //绑定在该对象中

            objc_setAssociatedObject(self, XWKVOBlockKey, allTargets, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

        }

        //获取对应keyPath中的所有target

        _XWBlockTarget *targetForKeyPath = allTargets[keyPath];

        if (!targetForKeyPath) {

            //没有则创建

            targetForKeyPath = [_XWBlockTarget new];

            //保存

            allTargets[keyPath] = targetForKeyPath;

            //如果第一次,则注册对keyPath的KVO监听

            [self addObserver:targetForKeyPath forKeyPath:keyPath options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

        }

        [targetForKeyPath xw_addBlock:block];

        //对第一次注册KVO的类进行dealloc方法调剂

        [self _xw_swizzleDealloc];

    }

    3、上一段代码的最后一个方法是对dealloc方法进行调剂,因为我们想要能够在合适的时候自动注销KVO,何为合适的地方呢,当然是被监听对象销毁的时候才是最合适的地方,所以dealloc方法里面是最合适的地方,我们期望能交换被监听对象的dealloc方法然后自己在该方法中实现注销KVO的逻辑,最先能想到的方式是通常我们使用的runtime中的swizzle黑魔法直接进行方法交换,但遗憾的是swizzle黑魔法只能在本类中交换本类的方法,而无法在一个类中对另一个类的方法进行调剂,所以需要另想调剂方法,我们采取直接对变监听对象所在的类修改或者添加dealloc方法来达到调剂目的,我结合代码进行说明:

    /**

    *  调剂dealloc方法,由于无法直接使用运行时的swizzle方法对dealloc方法进行调剂,所以稍微麻烦一些

    */

    - (void)_xw_swizzleDealloc{

        //我们给每个类绑定上一个值来判断dealloc方法是否被调剂过,因为一个类只需要调剂一次,如果调剂过了就无需再次调剂了

        BOOL swizzled = [objc_getAssociatedObject(self.class, deallocHasSwizzledKey) boolValue];

        //如果调剂过则直接返回

        if (swizzled) return;

        //开始调剂

        Class swizzleClass = self.class;

        //获取原有的dealloc方法

        SEL deallocSelector = sel_registerName("dealloc");

        //初始化一个函数指针用于保存原有的dealloc方法

        __block void (*originalDealloc)(__unsafe_unretained id, SEL) = NULL;

        //实现我们自己的dealloc方法,通过block的方式

        id newDealloc = ^(__unsafe_unretained id objSelf){

            //在这里我们移除所有的KVO

            [objSelf xw_removeAllObserverBlocks];

            //根据原有的dealloc方法是否存在进行判断

            if (originalDealloc == NULL) {//如果不存在,说明本类没有实现dealloc方法,则需要向父类发送dealloc消息(objc_msgSendSuper)

                //构造objc_msgSendSuper所需要的参数,.receiver为方法的实际调用者,即为类本身,.super_class指向其父类

                struct objc_super superInfo = {

                    .receiver = objSelf,

                    .super_class = class_getSuperclass(swizzleClass)

                };

                //构建objc_msgSendSuper函数

                void (*msgSend)(struct objc_super *, SEL) = (__typeof__(msgSend))objc_msgSendSuper;

                //向super发送dealloc消息

                msgSend(&superInfo, deallocSelector);

            }else{//如果存在,表明该类实现了dealloc方法,则直接调用即可

                //调用原有的dealloc方法

                originalDealloc(objSelf, deallocSelector);

            }

        };

        //根据block构建新的dealloc实现IMP

        IMP newDeallocIMP = imp_implementationWithBlock(newDealloc);

        //尝试添加新的dealloc方法,如果该类已经复写的dealloc方法则不能添加成功,反之则能够添加成功

        if (!class_addMethod(swizzleClass, deallocSelector, newDeallocIMP, "v@:")) {

            //如果没有添加成功则保存原有的dealloc方法,用于新的dealloc方法中,执行原有的系统的dealloc逻辑

            Method deallocMethod = class_getInstanceMethod(swizzleClass, deallocSelector);

            originalDealloc = (void(*)(__unsafe_unretained id, SEL))method_getImplementation(deallocMethod);

            originalDealloc = (void(*)(__unsafe_unretained id, SEL))method_setImplementation(deallocMethod, newDeallocIMP);

        }

        //标记该类已经调剂过了

        objc_setAssociatedObject(self.class, deallocHasSwizzledKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    }

    /**移除所有的KVO*/

    - (void)xw_removeAllObserverBlocks {

        NSMutableDictionary *allTargets = objc_getAssociatedObject(self, XWKVOBlockKey);

        if (!allTargets) return;

        [allTargets enumerateKeysAndObjectsUsingBlock:^(id key, _XWBlockTarget *target, BOOL *stop) {

            [self removeObserver:target forKeyPath:key];

        }];

        [allTargets removeAllObjects];

    }

    通过如上方式,我们就完成了对dealloc方法的调剂,新的dealloc方法执行的时候回注销注册的KVO,这样就免去了手动注销的麻烦事情咯!

    写在最后

    通知的大致实现方式和KVO一样,详情请自行查看代码咯,我就不多做说明了,现在终于能优雅愉快的使用KVO和通知了,复习一下github地址:https://github.com/wazrx/XWEasyKVONotification.git

  • 相关阅读:
    D. Constructing the Array
    B. Navigation System
    B. Dreamoon Likes Sequences
    A. Linova and Kingdom
    G. Special Permutation
    B. Xenia and Colorful Gems
    Firetrucks Are Red
    java getInstance()的使用
    java 静态代理和动态代理
    java 类加载机制和反射机制
  • 原文地址:https://www.cnblogs.com/fengmin/p/5679957.html
Copyright © 2011-2022 走看看