介绍 KVO( NSKeyValueObserving
)是一种监测对象属性值变化的观察者模式机制。其特点是无需事先修改被观察者代码,利用 runtime
实现运行中修改某一实例达到目的,保证了未侵入性。
A对象指定观察B对象的属性后,当属性发生变更,A对象会收到通知,获取变更前以及变更的状态,从而做进一步处理。
在实际生产环境中,多用于应用层观察模型层数据变动,接收到通知后更新,从而达成比较好的设计模式。
另一种常用的用法是 Debug
,通过观察问题属性的变化,追踪问题出现的堆栈,更有效率的解决问题。
应用 观察回调 1 2 3 4 - (void )observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id )object change:(nullable NSDictionary <NSKeyValueChangeKey , id > *)change context:(nullable void *)context;
观察者需要实现这个方法来接受回调,其中keyPath
是 KVC
路径, object
是观察者,context
区分不同观察的标识。
改变字典 最关键的是改变字典,其中包含了 NSKeyValueChangeKey
,通过预定义的字符串来获取特定的数值。
1 2 3 4 5 6 7 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
。
剩余的三种分别是插入、删除、替换,当观察的属性属于集合类(这点会在之后讲),变动时就会通知这些类型。
1 2 3 4 5 6 typedef NS_ENUM (NSUInteger , NSKeyValueChange ) { NSKeyValueChangeSetting = 1 , NSKeyValueChangeInsertion = 2 , NSKeyValueChangeRemoval = 3 , NSKeyValueChangeReplacement = 4 , };
NSKeyValueChangeNewKey
获取变更的最新值,NSKeyValueChangeOldKey
获取原始数值。
NSKeyValueChangeIndexesKey
如果观察的是集合,那这个键值返回索引集合。
NSKeyValueChangeNotificationIsPriorKey
如果设置了接受提前通知,那么修改之前会先发送通知,修改后再发一次。为了区分这两次,第一次会带上这个键值对,其内容为 @1
。
字符串枚举 在注册类型时,苹果使用了NS_STRING_ENUM
宏。
虽然这个宏在ObjC
下毫无作用,但是对于Swift
有优化 ,上面的定义会变成这样。1 2 3 4 5 6 7 8 9 enum NSKeyValueChangeKey : String { case kind case new case old case indexes case notificationIsPrior } let dict: [NSKeyValueChangeKey : Any ] = [......]let kind = dict[.kind] as ! Number
字符串枚举对于使用来说是非常直观和安全的。
添加与删除 对于普通对象,使用这两个方法就能注册与注销观察。1 2 3 4 5 6 7 8 - (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;
可以设置多种观察模式来匹配需求。1 2 3 4 5 6 7 8 9 10 11 typedef NS_OPTIONS (NSUInteger , NSKeyValueObservingOptions ) { NSKeyValueObservingOptionNew = 0x01 , NSKeyValueObservingOptionOld = 0x02 , NSKeyValueObservingOptionInitial = 0x04 , NSKeyValueObservingOptionPrior = 0x08 };
由于不符合 KVC
的访问器标准,苹果规定 NSArray NSOrderedSet NSSet
不可以执行 addObserver
方法,不然会抛出异常。针对 NSArray
有特殊的方法,如下1 2 3 4 5 6 7 8 9 10 - (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 NSKeyValueObserving
与 NSKeyValueCoding
一起定义在 Foundation
库,而这个库是不开源的,我们先从苹果开发者文档中获取信息。
Automatic key-value observing is implemented using a technique called isa-swizzling.
看描述猜测苹果应该是通过重新设置被观察者的 Class
(isa
中包含 Class
信息),该类继承了原类并且重载属性的 Setter
方法,添加发通知的操作达到目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @interface ConcreteSubject : NSObject @property (nonatomic , strong ) id obj;@end ConcreteSubject *sub = [ConcreteSubject new]; NSLog (@"%s" , class_getName(object_getClass(sub)));[sub addObserver:self forKeyPath:@"obj" options:NSKeyValueObservingOptionNew context:nil ]; NSLog (@"%s" , class_getName(object_getClass(sub)));NSLog (@"%s" , class_getName(object_getClass(class_getSuperclass(cls))));NSLog (@"%s" , class_getName(sub.class));class_getMethodImplementation(cls, @selector (setObj:)); class_getMethodImplementation(cls, @selector (class ));
试了一下果然 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
替换的实现 1 2 3 4 5 6 7 8 9 10 11 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
实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void __NSSetObjectValueAndNotify(id self , SEL _cmd, id value) { void *indexedIvars = object_getIndexedIvars(object_getClass(self )); pthread_mutex_lock(indexedIvars + 0x20 ); NSString *keyPath = [CFDictionaryGetValue (*(indexedIvars) + 0x18 ), _cmd) copyWithZone:0x0 ]; pthread_mutex_unlock(indexedIvars + 0x20 ); [self willChangeValueForKey:keyPath]; IMP imp = class_getMethodImplementation(*indexedIvars, _cmd); (imp)(self , _cmd, value); [self didChangeValueForKey:keyPath]; }
再看改变后的 Setter
方法,其中 indexedIvars
是原类之外的成员变量,第一个指针是改变后的类,0x20
的偏移量是线程锁,0x18
地址储存了改变过的方法字典。
在执行原方法实现前调用了 willChangeValueForKey
发起通知,同样在之后调用 didChangeValueForKey
。
添加观察方法 那么是在哪个方法中替换的实现呢?先看 [NSObject addObserver:forKeyPath:options:context:]
方法。1 2 3 4 5 6 7 8 9 10 11 12 13 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
代码,方便大家理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 - (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 , 1 ); pthread_mutex_lock(__NSKeyValueObserverRegistrationLock); } Info *info = __NSKeyValueRetainedObservationInfoForObject(self , property->_containerClass); id _additionOriginalObservable = nil ; if (option & NSKeyValueObservingOptionNew ) { 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 ]; Class cls = [property isaForAutonotifying]; if ((cls != NULL ) && (object_getClass(self ) != cls)) { object_setClass(self , cls); } [newInfo release]; if (info != nil ) { [info release]; } return ; }
其中有可能替换方法实现的步骤是获取 isa
的时候,猜测当第一次创建新类的时候,会注册新的方法,接着追踪 isaForAutonotifying
方法。
获取观察类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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:
是一个类方法,如果你不希望某个属性被观察,那么就设为 NO
,isa
返回是空也就宣告这次添加观察失败。
如果一切顺利的话,将会执行__NSKVONotifyingEnableForInfoAndKey(info, keyPath)
改变 class
的方法,最终返回其 isa
。
实质替换方法 由于该方法实在太长,且使用了goto
不方便阅读,所以依旧整理成伪代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 int __NSKVONotifyingEnableForInfoAndKey(void *info, id keyPath) { pthread_mutex_lock(info + 0x20 ); 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 ; } 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 ; } 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
是干嘛的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 int swizzleMutableMethod(void *info, id keyPath) { 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 } 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 } 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
中实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 int __NSKVONotifyingCreateInfoWithOriginalClass(Class cls) { const char *name = class_getName(cls); int length = strlen(r12) + 0x10 ; 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; *(indexedIvars + 0x8 ) = newCls; *(indexedIvars + 0x10 ) = CFSetCreateMutable (0x0 , 0x0 , _kCFCopyStringSetCallBacks); *(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); 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:)); }); } 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; _NSKVONotifyingSetMethodImplementation(indexedIvars, @selector (_isKVOA), _NSKVOIsAutonotifying, false ); _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>
返回依赖键值的字符串集合
1 2 3 4 5 6 7 8 9 10 11 12 + (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
方法中有这段代码吗?
1 2 [property object:self didAddObservance:newInfo recurse:true ];
其中 NSKeyValueProperty
也是一个类簇,具体分为 NSKeyValueProperty NSKeyValueComputedProperty NSKeyValueUnnestedProperty NSKeyValueNestedProperty
,从名字也看出 NSKeyValueNestedProperty
是指嵌套子属性的属性类,那我们观察下他的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 - (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
,并代码实现这两个通知方法,就可以达到手动触发的要求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + (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" ]; } }
如果操作的是之前提到的集合对象,那么实现的方法就需要变为
1 2 3 4 5 6 7 8 9 10 11 12 13 - (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;
依赖键观察 之前也有提过构建依赖关系的方法,具体操作如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 + (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); } }
可变数组与集合 不可变的数组与集合由于内部结构固定,所以只能通过观察容器类内存地址来判断是否变化,也就是 NSKeyValueChangeSetting
。
集合和数组的观察都很类似,我们先关注如果要观察可变数组内部插入移除的变化呢?
先了解一下集合代理方法,- (NSMutableArray *)mutableArrayValueForKey:
,这是一个 KVC
方法,能够返回一个可供观察的 NSKeyValueArray
对象。
根据苹果注释,其搜索顺序如下
1.搜索是否实现最少一个插入与一个删除方法1 2 3 4 -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:
第一种方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - (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 ]; }
通过实现了insert
与remove
方法,使得代理数组能够正常运作数组变量,KVO
观察了代理数组的这两个方法,发出了我们需要的通知。
这种方式使用了第一步搜索,比较容易理解,缺点是改动的代码比较多,改动数组必须通过自定义方法。
第二种方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @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 ]; }
这种方式相对来说更简洁,修改数组的方法与平时一致,比较适合使用。
下面说一下原理,首先我们没有实现对应的insert
与remove
方法,其次readonly
属性也没有set<key>:
方法,但我们实现了 @synthesize dataArray = _dataArray;
所以根据第三步对代理数组的操作都会实际操作到实例变量中。
然后重载了 dataArray
的 Getter
方法,保证了修改数组时必须调用主体是self.dataArray
,也就是代理数组,从而发送通知。
问答 KVO的底层实现? KVO
就是通过 Runtime
替换被观察类的 Setter
实现,从而在发生改变时发起通知。
如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)? 通过设置 automaticallyNotifiesObserversForKey
为 False
实现取消自动触发。
符合条件再触发可以这么实现。
1 2 3 4 5 6 7 8 9 10 11 12 - (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的一切)