美文网首页
iOS中KVO的巧妙使用及原理探究

iOS中KVO的巧妙使用及原理探究

作者: aksskas | 来源:发表于2018-03-16 13:31 被阅读0次

    YYKitNSObject+YYAddForKVO文件中以Block的形式包装了普通的KVO方法,觉得很是巧妙,由此引发了对KVO原理的探索。

    1.NSObject+YYAddForKVO的代码实现

    @interface _KVOWithBlockTarget_ : NSObject
    @property(nonatomic, copy) void (^block)(__weak id obj, __weak id oldVal, __weak id newVal);
    - (instancetype)initWithBlock:(void (^)(__weak id obj, __weak id oldVal, __weak id newVal))block;
    @end
    
    @implementation _KVOWithBlockTarget_
    - (instancetype)initWithBlock:(void (^)(__weak id, __weak id, __weak id))block {
        if (self = [super init]) {
            self.block = block;
        }
        return self;
    }
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {   
        if (!self.block) return;
        BOOL isPrior = [[change objectForKey: NSKeyValueChangeNotificationIsPriorKey] boolValue];
        if (isPrior) 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;
        self.block(object, oldVal, newVal);
    }
    @end
    
    static const int block_key;
    @implementation NSObject (KVOWithBlock)
    // 添加观察block
    - (void)addObserverForKeyPath:(NSString *)keyPath block:(void (^)(id, id, id))block {
        _KVOWithBlockTarget_ *target = [[_KVOWithBlockTarget_ alloc] init];
        NSMutableDictionary *dic = [self _allNSObjectObserverBlocks];
        NSMutableArray *arr = dic[keyPath];
        if (!arr) {
            arr = [NSMutableArray new];
            dic[keyPath] = arr;
        }
        [arr addObject:target];
        [self addObserver: target forKeyPath: keyPath options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context: NULL];
    }
    
    // 移除该对象某个keyPath对应的Blocks
    - (void)removeObserverBlocksForkeyPath: (NSString *)keyPath {
        if (!keyPath) return;
        NSMutableDictionary *dic = [self _allNSObjectObserverBlocks];
        NSMutableArray *arr = dic[keyPath];
        if (!arr) return;
        [arr enumerateObjectsUsingBlock:^(id target, NSUInteger index, BOOL *stop) {
            [self removeObserver:target forKeyPath:keyPath];
        }];
        [dic removeObjectForKey:keyPath];
    }
    
    // 移除该对象所有Blocks
    - (void)removeAllObserverBlocks {
        NSMutableDictionary *dic = [self _allNSObjectObserverBlocks];
        [dic enumerateKeysAndObjectsUsingBlock:^(NSString *keyPath, NSArray *arr, BOOL *stop) {
            [arr enumerateObjectsUsingBlock:^(id target, NSUInteger index, BOOL *stop) {
                [self removeObserver:target forKeyPath:keyPath];
            }];
        }];
        [dic removeAllObjects];
    }
    
    // 管理观察集合  [keyPath: [target]...] (多个keyPath,每个keyPath多个观察者)
    - (NSMutableDictionary *) _allNSObjectObserverBlocks {
        NSMutableDictionary *targets = objc_getAssociatedObject(self, &block_key);
        if (!targets) {
            targets = [NSMutableDictionary new];
            objc_setAssociatedObject(self, &block_key, targets, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        return targets;
    }
    @end
    

    分析:自定义_KVOWithBlockTarget_对象作为observer,然后自身通过数组字典来管理这些observer,包装非常巧妙,并未触及KVO的底层原理。

    2.KVO原理分析

    • KVO是基于runtime机制实现的,当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类(子类),A类的派生类名为NSKVONotifying_A。后面可能会应用到的方法objc_allocateClassPairobjc_registerClassPair
    • 每个类对象中都一个isa指针指向当前类,当一个类对象的属性第一次被观察时,那么系统会将isa指针指向动态生成的派生类,进而在赋值时执行的是派生类setter方法;而此时苹果为了隐藏该派生类,也重写了class方法,所以在执行[ojb class]方法时返回的依旧是原来的类。
    • KVO的观察通知则依赖于NSObject的两个方法 willChangeValueForKey:didChangevlueForKey:;在一个被观察属性发生改变之前,willChangeValueForKey:会被调用,这就记录旧值;而当发生改变之后,didChangevlueForKey:会被调用,继而observeValueForKey:ofObject:change:context: 也会被调用。所以可以预测伪码如下
    - (void)setName:(id)name {
          // 记录旧值
          [self willChangeValueForKey:];
          [super setValue: name forKey: @"name"];
          // 记录新值
          [self didChangevlueForKey:];  
    }
    
    • 以上,如果我们想手动触发KVO,可通过手动调用上面描述的两个方法
        // “手动触发self.name的KVO”,必写。
        [self willChangeValueForKey:@"name"];
    
        // “手动触发self.name的KVO”,必写。
        [self didChangeValueForKey:@"name"];
    
    • 原理图如下


      KVO原理图

    3.自定义实现简单的KVO机制

    NSObject+YYAddForKVO不同,这里我们完全自定义实现KVO机制,具体步骤如下

    1. 创建派生类,并仿照苹果隐藏派生类(重写class方法)
    2. 重写派生类的setter方法
    3. 类似YYAddForKVO,创建observer类来传递block(下面代码也是包装Block实现KVO)
    #import "NSObject+KVO.h"
    #import <objc/message.h>
    
    NSString *const kYYKVOClassPrefix = @"YYKVOClassPrefix_";
    NSString *const kYYKVOObservers = @"YYKVOObservers";
    
    typedef void(^YYKVOBlock)(__weak id observer, NSString *observerdKey, __weak id oldVal, __weak id newVal) ;
    
    //  observer类
    @interface YYObserveationTarget: NSObject
    @property (nonatomic, copy) NSString *key;
    @property (nonatomic, copy) YYKVOBlock block;
    
    - (instancetype)initWithObserverForKey: (NSString *)key block: (YYKVOBlock)block;
    @end
    
    @implementation YYObserveationTarget
    - (instancetype)initWithObserverForKey: (NSString *)key block: (YYKVOBlock)block {
        if (self = [super init]) {
            _key = key;
            _block = block;
        }
        return self;
    }
    @end
    
    static NSString * setterForGetter(NSString *getter) {
        if (getter.length <= 0) return nil;
        // 首字母大写
        NSString *firstLetter = [[getter substringToIndex:1] uppercaseString];
        NSString * remainLetters = [getter substringFromIndex:1];
        return [NSString stringWithFormat:@"set%@%@:",firstLetter,remainLetters];
    }
    static NSString * getterForSetter(NSString *setter) {
        if (setter.length <= 0 || ![setter hasPrefix:@"set"]) return nil;
        
        NSRange range = NSMakeRange(3, setter.length - 4);
        NSString *key = [setter substringWithRange:range];
        // 首字母小写
        NSString *firstLetter = [[key substringWithRange:NSMakeRange(0, 1)] lowercaseString];
        key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstLetter];
        return key;
    }
    
    static Class kvo_class(id self, SEL _cmd) {
        return class_getSuperclass(object_getClass(self));
    }
    
    static void kvo_setter(id self, SEL _cmd, id newVal) {
        NSString *setterName = NSStringFromSelector(_cmd);
        NSString *getterName = getterForSetter(setterName);
        if (!getterName) return;
        
        id oldValue = [self valueForKey:getterName];
        struct objc_super superClazz = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self))
        };
        // 防止编译器多参报错
        void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
        // 调用原类的setter方法(容易忘记)
        objc_msgSendSuperCasted(&superClazz, _cmd, newVal);
        
        NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kYYKVOObservers));
        //  Observer类来管理block的执行
        for (YYObserveationTarget *each in observers) {
            if ([each.key isEqualToString: getterName]) {
                // 在一个全局队列中执行block回调
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                    each.block(self, getterName, oldValue, newVal);
                });
            }
        }
    }
    
    
    @implementation NSObject (KVO)
    - (void)yy_addObserverForKey: (NSString *)key withBlock: (YYKVOBlock)block {
        
        // 0.key对应的setter等信息
        SEL setterSelector = NSSelectorFromString(setterForGetter(key));
        Method setterMethod = class_getInstanceMethod([self class], setterSelector);
        
        // 1. 判断是否为派生类,如果不是则创建
        Class cls = object_getClass(self);
        NSString *clsName = NSStringFromClass(cls);
        // 1.1如果不包含前缀,说明不是派生类
        if (![clsName hasPrefix:kYYKVOClassPrefix]) {
            // 1.2 创建派生类
            cls = [self makeKVOClass];
            // 1.3 将isa指针指向派生类
            object_setClass(self, cls);
        }
        
        // 2.重写派生类的setter方法,判断是否已经重写过了
        if (![self hasSelector:setterSelector]) {
            const char *types = method_getTypeEncoding(setterMethod);
            class_addMethod(cls, setterSelector, (IMP)kvo_setter, types);
        }
        
        // 3.管理observers
        NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)kYYKVOObservers);
        if (!observers) {
            observers = [NSMutableArray array];
            objc_setAssociatedObject(self, (__bridge const void *)kYYKVOObservers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        YYObserveationTarget *observer = [[YYObserveationTarget alloc]initWithObserverForKey: key block:block];
        [observers addObject: observer];
    }
    
    - (void)yy_removeObserverForKey: (NSString *)key {
        NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void*)kYYKVOObservers);
        NSMutableArray<YYObserveationTarget *> *infoToRemoveAry = [NSMutableArray array];
        for (YYObserveationTarget *observer in observers) {
            if ([observer.key isEqualToString:key]) {
                [infoToRemoveAry addObject:observer];
            }
        }
        [observers removeObjectsInArray:infoToRemoveAry];
    }
    
    - (Class)makeKVOClass {
        // 1.派生类类名
        NSString *kvoClsName = [kYYKVOClassPrefix stringByAppendingString:NSStringFromClass([self class])];
        Class kvoCls = NSClassFromString(kvoClsName);
        // 如果该类已经存在,则直接返回
        if (kvoCls) return kvoCls;
        
        Class originalCls = object_getClass(self);
        // 2. 动态创建该派生类,派生类继承自原类
        kvoCls = objc_allocateClassPair(originalCls, kvoClsName.UTF8String, 0);
        // 2.1 仿照苹果做法,重写class方法,隐藏该子类
        Method clsMethod = class_getInstanceMethod(originalCls, @selector(class));
        const char *types = method_getTypeEncoding(clsMethod);
        // kvo_class 返回父类类名,即返回原类,从而达到隐藏的效果
        class_addMethod(kvoCls, @selector(class), (IMP)kvo_class, types);
        // 2.2成对使用动态创建派生类
        objc_registerClassPair(kvoCls);
        
        return kvoCls;
    }
    
    - (BOOL)hasSelector: (SEL)selector {
        // 这里因为对象将isa指针指向了派生类,所以返回的应该是kvoCls
        Class cls = object_getClass(self);
        unsigned int methodCount = 0;
        Method *methodList = class_copyMethodList(cls, &methodCount);
        for (unsigned int i = 0 ; i < methodCount; i++) {
            SEL aSelector = method_getName(methodList[i]);
            if (aSelector == selector) {
                free(methodList);
                return YES;
            }
        }
        free(methodList);
        return NO;
    }
    @end
    

    可以看到代码中大量应用了runtime知识,因而熟悉runtime对我们学习iOS原理有很大的帮助。

    4.Swift4中KVO的使用方式

    1. 只有继承自NSObject类才能使用KVO
    2. 在Swift4中需要标记@objcMembersdynamic才能使用KVO
    3. 不在需要手动移除observer, 而是返回一个NSKeyValueObservation闭包,这带来了一个新的陷阱,我们需要主动去控制这个闭包的生命周期;如果没有对其强引用,该函数结束后闭包就会被回收。
    @objcMembers class KVOClass: NSObject {
        dynamic var name: String
        init(name: String) {
            self.name = name
        }
    }
     
    class ViewController: UIViewController {
     
        var aClass: KVOClass!
        var ob: NSKeyValueObservation!
     
        override func viewDidLoad() {
            super.viewDidLoad()
    
            aClass = KVOClass(name: "kvo")
            ob = aClass.observe(\.name) { (ob, changed) in
                let new = ob.name
                print(new)
            }
            aClass.name = "swift4"
        }
    }
    

    以上是对KVO的一点小结,主要参考了以下的几篇文章
    探究KVO的底层实现原理
    如何自己动手实现 KVO
    Swift 4新知:KVC和KVO新姿势

    相关文章

      网友评论

          本文标题:iOS中KVO的巧妙使用及原理探究

          本文链接:https://www.haomeiwen.com/subject/ibzgfftx.html