刨根问底KVO原理

介绍

KVO( NSKeyValueObserving )是一种监测对象属性值变化的观察者模式机制。其特点是无需事先修改被观察者代码,利用 runtime 实现运行中修改某一实例达到目的,保证了未侵入性。

A对象指定观察B对象的属性后,当属性发生变更,A对象会收到通知,获取变更前以及变更的状态,从而做进一步处理。

在实际生产环境中,多用于应用层观察模型层数据变动,接收到通知后更新,从而达成比较好的设计模式。

另一种常用的用法是 Debug,通过观察问题属性的变化,追踪问题出现的堆栈,更有效率的解决问题。


应用

观察回调

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context;

观察者需要实现这个方法来接受回调,其中keyPathKVC 路径, object 是观察者,context 区分不同观察的标识。

改变字典

最关键的是改变字典,其中包含了 NSKeyValueChangeKey,通过预定义的字符串来获取特定的数值。

typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;

FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey

NSKeyValueChangeKindKey 中定义的是改变的类型,如果调用的是Setter方法,那就是NSKeyValueChangeSetting

剩余的三种分别是插入、删除、替换,当观察的属性属于集合类(这点会在之后讲),变动时就会通知这些类型。

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};

NSKeyValueChangeNewKey 获取变更的最新值,NSKeyValueChangeOldKey 获取原始数值。

NSKeyValueChangeIndexesKey 如果观察的是集合,那这个键值返回索引集合。

NSKeyValueChangeNotificationIsPriorKey 如果设置了接受提前通知,那么修改之前会先发送通知,修改后再发一次。为了区分这两次,第一次会带上这个键值对,其内容为 @1

字符串枚举

在注册类型时,苹果使用了NS_STRING_ENUM宏。

虽然这个宏在ObjC下毫无作用,但是对于Swift有优化

,上面的定义会变成这样。

enum NSKeyValueChangeKey: String {
    case kind
    case new
    case old
    case indexes
    case notificationIsPrior
}
let dict: [NSKeyValueChangeKey : Any] = [......]
let kind = dict[.kind] as! Number

字符串枚举对于使用来说是非常直观和安全的。

添加与删除

对于普通对象,使用这两个方法就能注册与注销观察。

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(nullable void *)context;

- (void)removeObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
               context:(nullable void *)context;

可以设置多种观察模式来匹配需求。

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    //可以收到新改变的数值
    NSKeyValueObservingOptionNew = 0x01,
    //可以收到改变前的数值
    NSKeyValueObservingOptionOld = 0x02,
    //addObserver后立刻触发通知,只有new,没有old
    NSKeyValueObservingOptionInitial = 0x04,
    //会在改变前与改变后发送两次通知
    //改变前的通知带有[email protected],old
    NSKeyValueObservingOptionPrior = 0x08
};

由于不符合 KVC 的访问器标准,苹果规定 NSArray NSOrderedSet NSSet 不可以执行 addObserver 方法,不然会抛出异常。针对 NSArray 有特殊的方法,如下

- (void)addObserver:(NSObject *)observer
 toObjectsAtIndexes:(NSIndexSet *)indexes
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(nullable void *)context;

- (void)removeObserver:(NSObject *)observer
  fromObjectsAtIndexes:(NSIndexSet *)indexes
            forKeyPath:(NSString *)keyPath
               context:(nullable void *)context;

主要的区别在于多了一个ObjectsAtIndexes,其实做的事情是一样的,根据索引找到对象,再逐一建立观察关系。


原理

Runtime

NSKeyValueObservingNSKeyValueCoding 一起定义在 Foundation 库,而这个库是不开源的,我们先从苹果开发者文档中获取信息。

Automatic key-value observing is implemented using a technique called isa-swizzling.

看描述猜测苹果应该是通过重新设置被观察者的 Class (isa 中包含 Class 信息),该类继承了原类并且重载属性的 Setter 方法,添加发通知的操作达到目的。

@interface ConcreteSubject : NSObject
@property (nonatomic, strong) id obj;
@end

ConcreteSubject *sub = [ConcreteSubject new];

NSLog(@"%s", class_getName(object_getClass(sub)));
//改变前 outprint--> ConcreteSubject

[sub addObserver:self forKeyPath:@"obj" options:NSKeyValueObservingOptionNew context:nil];
//执行观察方法

NSLog(@"%s", class_getName(object_getClass(sub)));
//改变后 outprint--> NSKVONotifying_ConcreteSubject
NSLog(@"%s", class_getName(object_getClass(class_getSuperclass(cls))));
//获取超类名 outprint--> ConcreteSubject

NSLog(@"%s", class_getName(sub.class));
//获取类名 outprint--> ConcreteSubject

class_getMethodImplementation(cls, @selector(setObj:));
//imp = (IMP)(Foundation`_NSSetObjectValueAndNotify)

class_getMethodImplementation(cls, @selector(class));
//imp = (IMP)(Foundation`NSKVOClass)

试了一下果然 Class 被替换了,变成加了 NSKVONotifying_ 前缀的新类。

新类继承自原类,但是这个类的 class 方法返回的还是原类,这保证了外部逻辑完整。

反编译源码

通过 Runtime ,我们只能知道 KVO 使用了一个继承了原类的类,并且替换了原方法的实现,setObj: = _NSSetObjectValueAndNotify class = _NSKVOClass。如果我们想进一步了解详情,只能通过反编译 Foundation 来查找汇编代码。

这里我使用了 Hopper 工具,分析的二进制文件路径是/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation

替换的实现

//伪代码,仅供理解
void _NSKVOClass(id self,  SEL _cmd) {
    Class cls = object_getClass(self);
    Class originCls = __NSKVONotifyingOriginalClassForIsa(cls);
    if (cls != originCls) {
        return [originCls class];
    } else {
        Method method = class_getInstanceMethod(cls, _cmd);
        return method_invoke(self, method);
    }
}

先看原 class 方法,获取了当前类和原类,如果不一致就返回原类,如果一致就执行原 class 实现。

//伪代码,仅供理解
void __NSSetObjectValueAndNotify(id self, SEL _cmd, id value) {
    //获取额外的变量
    void *indexedIvars = object_getIndexedIvars(object_getClass(self));
    //加锁
    pthread_mutex_lock(indexedIvars + 0x20);
    //从SEL获取KeyPath
    NSString *keyPath = [CFDictionaryGetValue(*(indexedIvars) + 0x18), _cmd) copyWithZone:0x0];
    //解锁
    pthread_mutex_unlock(indexedIvars + 0x20);

    //改变前发通知
    [self willChangeValueForKey:keyPath];
    //实现Setter方法
    IMP imp = class_getMethodImplementation(*indexedIvars, _cmd);
    (imp)(self, _cmd, value);
    //改变后发通知
    [self didChangeValueForKey:keyPath];
}

再看改变后的 Setter 方法,其中 indexedIvars 是原类之外的成员变量,第一个指针是改变后的类,0x20 的偏移量是线程锁,0x18 地址储存了改变过的方法字典。

在执行原方法实现前调用了 willChangeValueForKey 发起通知,同样在之后调用 didChangeValueForKey

添加观察方法

那么是在哪个方法中替换的实现呢?先看 [NSObject addObserver:forKeyPath:options:context:] 方法。

//伪代码,仅供理解
void -[NSObject addObserver:forKeyPath:options:context:]
(void * self, void * _cmd, void * arg2, void * arg3, unsigned long long arg4, void * arg5) {
    pthread_mutex_lock(__NSKeyValueObserverRegistrationLock);
    *__NSKeyValueObserverRegistrationLockOwner = pthread_self();
    rax = object_getClass(self);
    rax = _NSKeyValuePropertyForIsaAndKeyPath(rax, arg3);
    [self _addObserver:arg2 forProperty:rax options:arg4 context:arg5];
    *__NSKeyValueObserverRegistrationLockOwner = 0x0;
    pthread_mutex_unlock(__NSKeyValueObserverRegistrationLock);

    return;
}

方法很简单,根据 KeyPath 获取具体属性后进一步调用方法。由于这个方法比较长,我特地整理成 ObjC 代码,方便大家理解。

//伪代码,仅供理解
- (void *)_addObserver:(id)observer
           forProperty:(NSKeyValueProperty *)property
               options:(NSKeyValueObservingOptions)option
               context:(void *)context {
    //需要注册通知
    if (option & NSKeyValueObservingOptionInitial) {
        //获取属性名路径
        NSString *keyPath = [property keyPath];
        //解锁
        pthread_mutex_unlock(__NSKeyValueObserverRegistrationLock);
        //如果注册了获得新值,就获取数值
        id value = nil;
        if (option & NSKeyValueObservingOptionNew) {
            value = [self valueForKeyPath:keyPath];
            if (value == nil) {
                value = [NSNull null];
            }
        }
        //发送注册通知
        _NSKeyValueNotifyObserver(observer, keyPath, self, context, value,
        0 /*originalObservable*/, 1 /*NSKeyValueChangeSetting*/);
        //加锁
        pthread_mutex_lock(__NSKeyValueObserverRegistrationLock);
    }
    //获取属性的观察信息
    Info *info = __NSKeyValueRetainedObservationInfoForObject(self, property->_containerClass);
    //判断是否需要获取新的数值
    id _additionOriginalObservable = nil;
    if (option & NSKeyValueObservingOptionNew) {
        //0x15没有找到定义,猜测为保存是否可观察的数组

        id tsd = _CFGetTSD(0x15);
        if (tsd != nil) {
            _additionOriginalObservable = *(tsd + 0x10);
        }
    }
    //在原有信息上生成新的信息
    Info *newInfo = __NSKeyValueObservationInfoCreateByAdding
    (info, observer, property, option, context, _additionOriginalObservable, 0, 1);
    //替换属性的观察信息
    __NSKeyValueReplaceObservationInfoForObject(self, property->_containerClass, info, newInfo);
    //属性添加后递归添加关联属性
    [property object:self didAddObservance:newInfo recurse:true];
    //获取新的isa
    Class cls = [property isaForAutonotifying];
    if ((cls != NULL) && (object_getClass(self) != cls)) {
        //如果是第一次就替换isa
        object_setClass(self, cls);
    }
    //释放观察信息
    [newInfo release];
    if (info != nil) {
        [info release];
    }
    return;
}

其中有可能替换方法实现的步骤是获取 isa 的时候,猜测当第一次创建新类的时候,会注册新的方法,接着追踪 isaForAutonotifying 方法。

获取观察类

void * -[NSKeyValueUnnestedProperty _isaForAutonotifying]
    (void * self, void * _cmd) {
    rbx = self;
    r14 = *_OBJC_IVAR_$_NSKeyValueProperty._containerClass;
    if ([*(rbx + r14)->_originalClass
        automaticallyNotifiesObserversForKey:rbx->_keyPath] != 0x0) {
            r14 = __NSKeyValueContainerClassGetNotifyingInfo(*(rbx + r14));
            if (r14 != 0x0) {
                    __NSKVONotifyingEnableForInfoAndKey(r14, rbx->_keyPath);
                    rax = *(r14 + 0x8);
            }
            else {
                    rax = 0x0;
            }
    }
    else {
            rax = 0x0;
    }
    return rax;
}

立刻发现了熟悉的方法!

automaticallyNotifiesObserversForKey: 是一个类方法,如果你不希望某个属性被观察,那么就设为 NOisa 返回是空也就宣告这次添加观察失败。

如果一切顺利的话,将会执行__NSKVONotifyingEnableForInfoAndKey(info, keyPath) 改变 class 的方法,最终返回其 isa

实质替换方法

由于该方法实在太长,且使用了goto不方便阅读,所以依旧整理成伪代码。

//伪代码,仅供理解
int __NSKVONotifyingEnableForInfoAndKey(void *info, id keyPath) {
    //线程锁加锁
    pthread_mutex_lock(info + 0x20);
    //添加keyPath到数组
    CFSetAddValue(*(info + 0x10), keyPath);
    //解锁
    pthread_mutex_unlock(info + 0x20);
    //判断原类实现能不能替换
    Class originClass = *info;
    MethodClass *methodClass =
    __NSKeyValueSetterForClassAndKey(originClass, keyPath, originClass);
    if (![methodClass isKindOfClass:[NSKeyValueMethodSetter class]]) {
        swizzleMutableMethod(info, keyPath);
        return;
    }
    //判断Setter方法返回值
    Method method = [methodClass method];
    if (*(int8_t *)method_getTypeEncoding(method) != _C_VOID) {
        _NSLog(@"KVO autonotifying only supports -set<Key>: methods that return void.");
        swizzleMutableMethod(info, keyPath);
        return;
    }
    //获取Setter方法参数
    char *typeEncoding = method_copyArgumentType(method, 0x2);
    char type = sign_extend_64(*(int8_t *)typeEncoding);
    SEL sel;//根据参数类型选择替换的方法
    switch (type) {
        case _C_BOOL: sel = __NSSetBoolValueAndNotify;
        case _C_UCHR: sel = __NSSetUnsignedCharValueAndNotify;
        case _C_UINT: sel = __NSSetUnsignedIntValueAndNotify;
        case _C_ULNG: sel = __NSSetUnsignedLongValueAndNotify;
        case _C_ULNG_LNG: sel = __NSSetUnsignedLongLongValueAndNotify;
        case _C_CHR: sel = __NSSetCharValueAndNotify;
        case _C_DBL: sel = __NSSetDoubleValueAndNotify;
        case _C_FLT: sel = __NSSetFloatValueAndNotify;
        case _C_INT: sel = __NSSetIntValueAndNotify;
        case _C_LNG: sel = __NSSetLongValueAndNotify;
        case _C_LNG_LNG: sel = __NSSetLongLongValueAndNotify;
        case _C_SHT: sel = __NSSetShortValueAndNotify;
        case _C_USHT: sel = __NSSetUnsignedShortValueAndNotify;
        case _C_LNG_LNG: sel = __NSSetLongLongValueAndNotify;
        case _C_ID: sel = __NSSetObjectValueAndNotify;
        case "{CGPoint=dd}": sel = __NSSetPointValueAndNotify;
        case "{_NSRange=QQ}": sel = __NSSetRangeValueAndNotify;
        case "{CGRect={CGPoint=dd}{CGSize=dd}}": sel = __NSSetRectValueAndNotify;
        case "{CGSize=dd}": sel = __NSSetSizeValueAndNotify;
        case *_NSKeyValueOldSizeObjCTypeName: sel = __CF_forwarding_prep_0;
        default;
    }
    //不支持的参数类型打印错误信息
    if (sel == NULL) {
        _NSLog(@"KVO autonotifying only supports -set<Key>: methods that take id,
        NSNumber-supported scalar types, and some NSValue-supported structure types.")
        swizzleMutableMethod(info, keyPath);
        return;
    }
    //替换方法实现
    SEL methodSel = method_getName(method);
    _NSKVONotifyingSetMethodImplementation(info, methodSel, sel, keyPath);
    if (sel == __CF_forwarding_prep_0) {
        _NSKVONotifyingSetMethodImplementation(info, @selector(forwardInvocation:),
         _NSKVOForwardInvocation, false);
        Class cls = *(info + 0x8);
        SEL newSel = sel_registerName("_original_" + sel_getName(methodSel));
        Imp imp = method_getImplementation(method);
        TypeEncoding type = method_getTypeEncoding(method);
        class_addMethod(cls, newSel, imp, type);
    }
    swizzleMutableMethod(info, keyPath);
}

可以表述为根据 Setter 方法输入参数类型,匹配合适的 NSSetValueAndNotify 实现来替换,从而实现效果。

那么 swizzleMutableMethod 是干嘛的呢?

//替换可变数组集合的方法
int swizzleMutableMethod(void *info, id keyPath) {
    //NSKeyValueArray
    CFMutableSetRef getterSet = __NSKeyValueMutableArrayGetterForIsaAndKey(*info, keyPath);
    if ([getterSet respondsToSelector:mutatingMethods]) {
        mutatingMethods methodList = [getterSet mutatingMethods];
        replace methodList->insertObjectAtIndex _NSKVOInsertObjectAtIndexAndNotify
        replace methodList->insertObjectsAtIndexes _NSKVOInsertObjectsAtIndexesAndNotify
        replace methodList->removeObjectAtIndex _NSKVORemoveObjectAtIndexAndNotify
        replace methodList->removeObjectsAtIndexes _NSKVORemoveObjectsAtIndexesAndNotify
        replace methodList->replaceObjectAtIndex _NSKVOReplaceObjectAtIndexAndNotify
        replace methodList->replaceObjectsAtIndexes _NSKVOReplaceObjectsAtIndexesAndNotify
    }
    //NSKeyValueOrderedSet
    getterSet = __NSKeyValueMutableOrderedSetGetterForIsaAndKey(*info, keyPath);
    if ([getterSet respondsToSelector:mutatingMethods]) {
        mutatingMethods methodList = [getterSet mutatingMethods];
        replace methodList->insertObjectAtIndex _NSKVOInsertObjectAtIndexAndNotify
        replace methodList->insertObjectsAtIndexes _NSKVOInsertObjectsAtIndexesAndNotify
        replace methodList->removeObjectAtIndex _NSKVORemoveObjectAtIndexAndNotify
        replace methodList->removeObjectsAtIndexes _NSKVORemoveObjectsAtIndexesAndNotify
        replace methodList->replaceObjectAtIndex _NSKVOReplaceObjectAtIndexAndNotify
        replace methodList->replaceObjectsAtIndexes _NSKVOReplaceObjectsAtIndexesAndNotify
    }
    //NSKeyValueSet
    getterSet = __NSKeyValueMutableSetGetterForClassAndKey(*info, keyPath);
    if ([getterSet respondsToSelector:mutatingMethods]) {
        mutatingMethods methodList = [getterSet mutatingMethods];
        replace methodList->addObject _NSKVOAddObjectAndNotify
        replace methodList->intersectSet _NSKVOIntersectSetAndNotify
        replace methodList->minusSet _NSKVOMinusSetAndNotify
        replace methodList->removeObject _NSKVORemoveObjectAndNotify
        replace methodList->unionSet _NSKVOUnionSetAndNotify
    }
    //改变新类的方法缓存
    __NSKeyValueInvalidateCachedMutatorsForIsaAndKey(*(info + 0x8), keyPath);
    return rax;
}

前面提到的都是一对一,那如果我想观察一对多的集合类呢?就是通过 KVC 中的 mutableArrayValueForKey: 返回一个代理集合,改变这些代理类的实现做到的。具体的例子之后会介绍。

创建新类

还有一个疑问就是替换的类是怎么创建的?具体方法在 __NSKVONotifyingEnableForInfoAndKey 中实现。

//伪代码,仅供理解
int __NSKVONotifyingCreateInfoWithOriginalClass(Class cls) {
    //拼接新名字
    const char *name = class_getName(cls);
    int length = strlen(r12) + 0x10;//16是NSKVONotifying_的长度
    char *newName = malloc(length);
    __strlcpy_chk(newName, "NSKVONotifying_", length, -1);
    __strlcat_chk(newName, name, length, -1);
    //生成一个继承原类的新类
    Class newCls = objc_allocateClassPair(cls, newName, 0x68);
    free(newName);
    if (newCls != NULL) {
        objc_registerClassPair(newCls);
        //获取额外的实例变量表
        void *indexedIvars = object_getIndexedIvars(newCls);
        *indexedIvars = cls;            //记录原isa
        *(indexedIvars + 0x8) = newCls; //记录新isa
        //新建一个集合,保存观察的keyPath
        *(indexedIvars + 0x10) = CFSetCreateMutable(0x0, 0x0, _kCFCopyStringSetCallBacks);
        //新建一个字典,保存改变过的SEL
        *(indexedIvars + 0x18) = CFDictionaryCreateMutable(0x0, 0x0, 0x0,
                                _kCFTypeDictionaryValueCallBacks);
        //新建一个线程锁
        pthread_mutexattr_init(var_38);
        pthread_mutexattr_settype(var_38, 0x2);
        pthread_mutex_init(indexedIvars + 0x20, var_38);
        pthread_mutexattr_destroy(var_38);
        //获取NSObject类默认的实现
        if (*__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectIMPLookupOnce == NULL) {
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                *__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange =
                class_getMethodImplementation([NSObject class],
                @selector(willChangeValueForKey:));

                *__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange =
                class_getMethodImplementation([NSObject class],
                @selector(didChangeValueForKey:));
            });
        }
        //设置是否替换过ChangeValue方法的flag
        BOOL isChangedImp = YES;
        if (class_getMethodImplementation(cls, @selector(willChangeValueForKey:)) ==
        *__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange) {
            BOOL isChangedDidImp =
                class_getMethodImplementation(cls, @selector(didChangeValueForKey:))
                !=
                *__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange;
            isChangedImp = isChangedDidImp ? YES : NO;
        }
        *(int8_t *)(indexedIvars + 0x60) = isChangedImp;

        //使用KVO的实现替换原类方法
        _NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(_isKVOA),
         _NSKVOIsAutonotifying, false/*是否需要保存SEL到字典*/);

        _NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(dealloc),
         _NSKVODeallocate, false);

        _NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(class),
         _NSKVOClass, false);
    }
    return newCls;
}

建立关系

还有一种情况就是观察的属性依赖于多个关系,比如 color 可能依赖于 r g b a,其中任何一个改变,都需要通知 color 的变化。

建立关系的方法是

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key

+ (NSSet *)keyPathsForValuesAffecting<key>

返回依赖键值的字符串集合

//伪代码
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    char *str = "keyPathsForValuesAffecting" + key;
    SEL sel = sel_registerName(str);
    Method method = class_getClassMethod(self, sel);
    if (method != NULL) {
        result = method_invoke(self, method);
    } else {
        result = [self _keysForValuesAffectingValueForKey:key];
    }
    return result;
}

还记得之前在 _addObserver 方法中有这段代码吗?

//属性添加后递归添加关联属性
[property object:self didAddObservance:newInfo recurse:true];

其中 NSKeyValueProperty 也是一个类簇,具体分为 NSKeyValueProperty NSKeyValueComputedProperty NSKeyValueUnnestedProperty NSKeyValueNestedProperty,从名字也看出 NSKeyValueNestedProperty 是指嵌套子属性的属性类,那我们观察下他的实现。

//伪代码
- (void)object:(id)obj didAddObservance:(id)info recurse:(BOOL)isRecurse {
    if (self->_isAllowedToResultInForwarding != nil) {
        //获得关系键
        relateObj = [obj valueForKey:self->_relationshipKey];
        //注册所有关系通知
        [relateObj addObserver:info
                    forKeyPath:self->_keyPathFromRelatedObject
                       options:info->options
                       context:nil];
    }
    //再往下递归
    [self->_relationshipProperty object:obj didAddObservance:info recurse:isRecurse];
}

至此,实现的大致整体轮廓比较了解了,下面会讲一下怎么把原理运用到实际。


应用原理

手动触发

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key 返回是 YES,那么注册的这个 Key 就会替换对应的 Setter ,从而在改变的时候调用 -(void)willChangeValueForKey:(NSString *)key-(void)didChangeValueForKey:(NSString *)key 发送通知给观察者。

那么只要把自动通知设为 NO,并代码实现这两个通知方法,就可以达到手动触发的要求。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"object"]) {
        return false;
    }

    return [super automaticallyNotifiesObserversForKey:key];
}

- (void)setObject:(NSObject *)object {
    if (object != _object) {
        [self willChangeValueForKey:@"object"];
        _object = object;
        [self didChangeValueForKey:@"object"];
    }
}

如果操作的是之前提到的集合对象,那么实现的方法就需要变为

- (void)willChange:(NSKeyValueChange)changeKind
   valuesAtIndexes:(NSIndexSet *)indexes
            forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind
  valuesAtIndexes:(NSIndexSet *)indexes
           forKey:(NSString *)key;

- (void)willChangeValueForKey:(NSString *)key
              withSetMutation:(NSKeyValueSetMutationKind)mutationKind
                 usingObjects:(NSSet *)objects;
- (void)didChangeValueForKey:(NSString *)key
             withSetMutation:(NSKeyValueSetMutationKind)mutationKind
                usingObjects:(NSSet *)objects;

依赖键观察

之前也有提过构建依赖关系的方法,具体操作如下

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    if ([key isEqualToString:@"color"]) {
        return [NSSet setWithObjects:@"r",@"g",@"b",@"a",nil];
    }

    return [super keyPathsForValuesAffectingValueForKey:key];
}

//建议使用静态指针地址作为上下文区分不同的观察
static void * const kColorContext = (void*)&kColorContext;
- (void)viewDidLoad {
    [super viewDidLoad];

    [self addObserver:self forKeyPath:@"color"
              options:NSKeyValueObservingOptionNew
              context:kColorContext];
    self.r = 133;
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if (context == kColorContext) {
        NSLog(@"%@", keyPath);
        //outprint --> color
    }
}

可变数组与集合

不可变的数组与集合由于内部结构固定,所以只能通过观察容器类内存地址来判断是否变化,也就是 NSKeyValueChangeSetting

集合和数组的观察都很类似,我们先关注如果要观察可变数组内部插入移除的变化呢?

先了解一下集合代理方法,- (NSMutableArray *)mutableArrayValueForKey:,这是一个 KVC 方法,能够返回一个可供观察的 NSKeyValueArray 对象。

根据苹果注释,其搜索顺序如下

1.搜索是否实现最少一个插入与一个删除方法

-insertObject:in<Key>AtIndex:
-removeObjectFrom<Key>AtIndex:
-insert<Key>:atIndexes:
-remove<Key>AtIndexes:

2.否则搜索是否有 set<Key>: 方法,有的话每次都把修改数组重新赋值回原属性。

3.否则检查 + (BOOL)accessInstanceVariablesDirectly,如果是YES,就查找成员变量_<key> or <key>,此后所有的操作针对代理都转接给成员变量执行。

4.最后进入保护方法valueForUndefinedKey:

第一种方法

- (void)insertObject:(NSObject *)object inDataArrayAtIndex:(NSUInteger)index {
    [_dataArray insertObject:object atIndex:index];
}

- (void)removeObjectFromDataArrayAtIndex:(NSUInteger)index {
    [_dataArray removeObjectAtIndex:index];
}

- (void)viewDidLoad {
    [super viewDidLoad];

    _dataArray = @[].mutableCopy;
    [self addObserver:self forKeyPath:@"dataArray"
    options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld |
    NSKeyValueObservingOptionPrior context:nil];
    [self insertObject:@1 inDataArrayAtIndex:0];
}

通过实现了insertremove方法,使得代理数组能够正常运作数组变量,KVO 观察了代理数组的这两个方法,发出了我们需要的通知。

这种方式使用了第一步搜索,比较容易理解,缺点是改动的代码比较多,改动数组必须通过自定义方法。

第二种方法

@property (nonatomic, strong, readonly) NSMutableArray *dataArray;

@synthesize dataArray = _dataArray;

- (NSMutableArray *)dataArray {
    return [self mutableArrayValueForKey:@"dataArray"];
}

- (void)viewDidLoad {
    [super viewDidLoad];

    _dataArray = @[].mutableCopy;
    [self addObserver:self forKeyPath:@"dataArray"
    options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld |
    NSKeyValueObservingOptionPrior context:nil];
    [self.dataArray addObject:@1];
}

这种方式相对来说更简洁,修改数组的方法与平时一致,比较适合使用。

下面说一下原理,首先我们没有实现对应的insertremove方法,其次readonly属性也没有set<key>:方法,但我们实现了 @synthesize dataArray = _dataArray; 所以根据第三步对代理数组的操作都会实际操作到实例变量中。

然后重载了 dataArrayGetter 方法,保证了修改数组时必须调用主体是self.dataArray,也就是代理数组,从而发送通知。


问答

KVO的底层实现?

KVO 就是通过 Runtime 替换被观察类的 Setter 实现,从而在发生改变时发起通知。

如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?

通过设置 automaticallyNotifiesObserversForKeyFalse 实现取消自动触发。

符合条件再触发可以这么实现。

- (void)setObject:(NSObject *)object {
    if (object == _object) return;

    BOOL needNotify = [object isKindOfClass:[NSString class]];
    if (needNotify) {
        [self willChangeValueForKey:@"object"];
    }
    _object = object;
    if (needNotify) {
        [self didChangeValueForKey:@"object"];
    }
}

总结

由于对汇编语言、反编译工具、objc4开源代码的不熟悉,这篇文章写了一周时间,结构也有点混乱。

所幸还是理顺了整体结构,在整理的过程中学会了很多很多。

由于才疏学浅,其中对汇编和源码的解释难免出错,还望大佬多多指教!


资料分享

ObjC中国的期刊 KVC和KVO

杨大牛的 Objective-C中的KVC和KVO

iOS开发技巧系列---详解KVC(我告诉你KVC的一切)

原文地址:https://www.cnblogs.com/vanch/p/9795279.html

时间: 2024-11-05 19:44:43

刨根问底KVO原理的相关文章

KVC/KVO原理详解及编程指南

http://blog.csdn.net/wzzvictory/article/details/9674431 2.KVC/KVO实现原理 键值编码和键值观察是根据isa-swizzling技术来实现的,主要依据runtime的强大动态能力.下面的这段话是引自网上的一篇文章: http://blog.csdn.net/kesalin/article/details/8194240 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的

KVC/KVO原理详解及编程指南(转载)

KVC/KVO原理详解及编程指南 作者:wangzz 原文地址:http://blog.csdn.net/wzzvictory/article/details/9674431 转载请注明出处 如果觉得文章对你有所帮助,请通过留言或关注微信公众帐号wangzzstrive来支持我,谢谢! 前言: 1.本文基本不讲KVC/KVO的用法,只结合网上的资料说说对这种技术的理解. 2.由于KVO内容较少,而且是以KVC为基础实现的,本文将着重介绍KVC部分. 一.简介 KVC/KVO是观察者模式的一种实现

转:KVC/KVO原理详解及编程指南

作者:wangzz 原文地址:http://blog.csdn.net/wzzvictory/article/details/9674431 转载请注明出处 如果觉得文章对你有所帮助,请通过留言或关注微信公众帐号wangzzstrive来支持我,谢谢! 前言: 1.本文基本不讲KVC/KVO的用法,只结合网上的资料说说对这种技术的理解. 2.由于KVO内容较少,而且是以KVC为基础实现的,本文将着重介绍KVC部分. 一.简介 KVC/KVO是观察者模式的一种实现,在Cocoa中是以被万物之源NS

ios-kvc\kvo 原理

原文地址:http://blog.csdn.net/wzzvictory/article/details/9674431 KVC(Key-value coding)键值编码,类似于map,提供了一种使用字符串而不是访问器方法去访问一个对象实例变量的机制. KVO(Key-value observing)键值观察,提供了一种当其它对象属性被修改的时候能通知当前对象的机制. 1.Key和Key Path KVC定义了一种按名称访问对象属性的机制,支持这种访问的主要方法是: - (id)valueFo

KVO 原理个人理解

期初,这个是因为朋友说的,忘记是哪所大公司的笔试题,说 KVO 的实现为什么要创建一个子类.当初,我还瞎说的做了一个回答,感觉这个也没啥呢.随着自己兴趣使然,我就简单搜索了一下,我天,铺天盖地的文献,才知道,原来这个也是个领域哦. 后来就慢慢的接触.了解,也听说了很多新词,其中陌生的要属 isa 指针了,因为当初这个东西只是在打断点的时候看过,但是从来不知道是干啥的,也没有深究过,另外一个是 Runtime 机制,好像一下子就把这个天天用也天天不在意的 KVO 提高了很高很高的档次,"有三四层楼

通过子类实现KVO,浅析KVO底层原理

通过手动实现KVO,对KVO底层原理有一定认识. KVO只要是通过监听set方法,从而实现对该对象的监听. 要监听set方法,有两种实现方式,第一就是使用分类,重写set方法,但是这样就会覆盖父类的set方法,所以不可行,pass掉. 第二就是使用子类,把父类的isa指针改为子类.然后调用父类色set方法,最后调用回调方法,该方案可行. 首先是注册监听,在调用监听方法的时候,会动态实现子类,把observer保存到子类的属性中(弱引用weak类型,不能使用strong,会造成循环引用),并且把类

IOS开发——UI基础-KVO

KVO == Key Value Observing 作用: 可以监听某个对象属性的改变 一.使用KVO Person *p = [Person new]; p.name = @"chg"; p.age = 30; // 给p这个对象添加一个监听 , 监听p对象的age属性的改变, 只要age属性改变就通知self [p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld |

线程特定数据TSD总结

一线程的本质 二线程模型的引入 三线程特定数据 四关键函数说明 五刨根问底啥原理 六私有数据使用示例 七参考文档 一.线程的本质 Linux线程又称轻量进程(LWP),也就说线程本质是用进程之间共享用户空间模拟实现的. 二.线程模型的引入 线程模型引入是为了数据共享,为什么又引入线程私有数据?有时候想让基于进程的接口适应多线程环境,这时候就需要为每个线程维护一份私有数据了,最典型的就是errno了. 在维护每个线程的私有数据的时候,我们可能会想到分配一个保存线程数据的数组,用线程的ID作为数组的

Runtime ----- 带你上道

在IOS开发和学习过程中,我们经常会接触到一个词: Runtime  .很多开发者对之既熟悉又陌生,基本都是浅尝辄止,达不到灵活使用的水平(话说开发中也确实不经常用..)本文和大家一起研究一下,Runtime到底是什么,还有他的一些应用场景,毕竟Runtime是OC动态特性的核心,熟练掌握它可以帮助我们更好的控制类的属性及方法,编写出更高效的代码. 一.什么是Runtime  不管你之前如何理解的Runtime,先把他扔一边,我们从头梳理一下: 1.有一种大气而准确的说法 : Objective