转载链接:https://juejin.im/post/5c22023df265da6124157a25
介绍
KVO( NSKeyValueObserving
)是一种监测对象属性值变化的观察者模式机制。其特点是无需事先修改被观察者代码,利用 runtime
实现运行中修改某一实例达到目的,保证了未侵入性。
A对象指定观察B对象的属性后,当属性发生变更,A对象会收到通知,获取变更前以及变更的状态,从而做进一步处理。
在实际生产环境中,多用于应用层观察模型层数据变动,接收到通知后更新,从而达成比较好的设计模式。
另一种常用的用法是 Debug
,通过观察问题属性的变化,追踪问题出现的堆栈,更有效率的解决问题。
应用
观察回调
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionaryid > *)change context:(nullable void *)context;
观察者需要实现这个方法来接受回调,其中keyPath
是 KVC
路径, object
是观察者,context
区分不同观察的标识。
改变字典
最关键的是改变字典,其中包含了 NSKeyValueChangeKey
,通过预定义的字符串来获取特定的数值。
1 typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
2
3 FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
4 FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
5 FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
6 FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
7 FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey
NSKeyValueChangeKindKey
中定义的是改变的类型,如果调用的是Setter
方法,那就是NSKeyValueChangeSetting
。
剩余的三种分别是插入、删除、替换,当观察的属性属于集合类(这点会在之后讲),变动时就会通知这些类型。
1 typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
2 NSKeyValueChangeSetting = 1,
3 NSKeyValueChangeInsertion = 2,
4 NSKeyValueChangeRemoval = 3,
5 NSKeyValueChangeReplacement = 4,
6 };
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
字符串枚举对于使用来说是非常直观和安全的。
添加与删除
对于普通对象,使用这两个方法就能注册与注销观察。
1 - (void)addObserver:(NSObject *)observer
2 forKeyPath:(NSString *)keyPath
3 options:(NSKeyValueObservingOptions)options
4 context:(nullable void *)context;
5
6 - (void)removeObserver:(NSObject *)observer
7 forKeyPath:(NSString *)keyPath
8 context:(nullable void *)context;
可以设置多种观察模式来匹配需求。
1 typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
2 //可以收到新改变的数值
3 NSKeyValueObservingOptionNew = 0x01,
4 //可以收到改变前的数值
5 NSKeyValueObservingOptionOld = 0x02,
6 //addObserver后立刻触发通知,只有new,没有old
7 NSKeyValueObservingOptionInitial = 0x04,
8 //会在改变前与改变后发送两次通知
9 //改变前的通知带有notificationIsPrior=@1,old
10 NSKeyValueObservingOptionPrior = 0x08
11 };
由于不符合 KVC
的访问器标准,苹果规定 NSArray NSOrderedSet NSSet
不可以执行 addObserver
方法,不然会抛出异常。针对 NSArray
有特殊的方法,如下
1 - (void)addObserver:(NSObject *)observer
2 toObjectsAtIndexes:(NSIndexSet *)indexes
3 forKeyPath:(NSString *)keyPath
4 options:(NSKeyValueObservingOptions)options
5 context:(nullable void *)context;
6
7 - (void)removeObserver:(NSObject *)observer
8 fromObjectsAtIndexes:(NSIndexSet *)indexes
9 forKeyPath:(NSString *)keyPath
10 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 @interface ConcreteSubject : NSObject
2 @property (nonatomic, strong) id obj;
3 @end
4
5 ConcreteSubject *sub = [ConcreteSubject new];
6
7 NSLog(@"%s", class_getName(object_getClass(sub)));
8 //改变前 outprint--> ConcreteSubject
9
10 [sub addObserver:self forKeyPath:@"obj" options:NSKeyValueObservingOptionNew context:nil];
11 //执行观察方法
12
13 NSLog(@"%s", class_getName(object_getClass(sub)));
14 //改变后 outprint--> NSKVONotifying_ConcreteSubject
15 NSLog(@"%s", class_getName(object_getClass(class_getSuperclass(cls))));
16 //获取超类名 outprint--> ConcreteSubject
17
18 NSLog(@"%s", class_getName(sub.class));
19 //获取类名 outprint--> ConcreteSubject
20
21 class_getMethodImplementation(cls, @selector(setObj:));
22 //imp = (IMP)(Foundation`_NSSetObjectValueAndNotify)
23
24 class_getMethodImplementation(cls, @selector(class));
25 //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
替换的实现
1 //伪代码,仅供理解
2 void _NSKVOClass(id self, SEL _cmd) {
3 Class cls = object_getClass(self);
4 Class originCls = __NSKVONotifyingOriginalClassForIsa(cls);
5 if (cls != originCls) {
6 return [originCls class];
7 } else {
8 Method method = class_getInstanceMethod(cls, _cmd);
9 return method_invoke(self, method);
10 }
11 }
先看原 class
方法,获取了当前类和原类,如果不一致就返回原类,如果一致就执行原 class
实现。
1 //伪代码,仅供理解
2 void __NSSetObjectValueAndNotify(id self, SEL _cmd, id value) {
3 //获取额外的变量
4 void *indexedIvars = object_getIndexedIvars(object_getClass(self));
5 //加锁
6 pthread_mutex_lock(indexedIvars + 0x20);
7 //从SEL获取KeyPath
8 NSString *keyPath = [CFDictionaryGetValue(*(indexedIvars) + 0x18), _cmd) copyWithZone:0x0];
9 //解锁
10 pthread_mutex_unlock(indexedIvars + 0x20);
11
12 //改变前发通知
13 [self willChangeValueForKey:keyPath];
14 //实现Setter方法
15 IMP imp = class_getMethodImplementation(*indexedIvars, _cmd);
16 (imp)(self, _cmd, value);
17 //改变后发通知
18 [self didChangeValueForKey:keyPath];
19 }
再看改变后的 Setter
方法,其中 indexedIvars
是原类之外的成员变量,第一个指针是改变后的类,0x20
的偏移量是线程锁,0x18
地址储存了改变过的方法字典。
在执行原方法实现前调用了 willChangeValueForKey
发起通知,同样在之后调用 didChangeValueForKey
。
添加观察方法
那么是在哪个方法中替换的实现呢?先看 [NSObject addObserver:forKeyPath:options:context:]
方法。
1 //伪代码,仅供理解
2 void -[NSObject addObserver:forKeyPath:options:context:]
3 (void * self, void * _cmd, void * arg2, void * arg3, unsigned long long arg4, void * arg5) {
4 pthread_mutex_lock(__NSKeyValueObserverRegistrationLock);
5 *__NSKeyValueObserverRegistrationLockOwner = pthread_self();
6 rax = object_getClass(self);
7 rax = _NSKeyValuePropertyForIsaAndKeyPath(rax, arg3);
8 [self _addObserver:arg2 forProperty:rax options:arg4 context:arg5];
9 *__NSKeyValueObserverRegistrationLockOwner = 0x0;
10 pthread_mutex_unlock(__NSKeyValueObserverRegistrationLock);
11
12 return;
13 }
方法很简单,根据 KeyPath
获取具体属性后进一步调用方法。由于这个方法比较长,我特地整理成 ObjC
代码,方便大家理解。
1 //伪代码,仅供理解
2 - (void *)_addObserver:(id)observer
3 forProperty:(NSKeyValueProperty *)property
4 options:(NSKeyValueObservingOptions)option
5 context:(void *)context {
6 //需要注册通知
7 if (option & NSKeyValueObservingOptionInitial) {
8 //获取属性名路径
9 NSString *keyPath = [property keyPath];
10 //解锁
11 pthread_mutex_unlock(__NSKeyValueObserverRegistrationLock);
12 //如果注册了获得新值,就获取数值
13 id value = nil;
14 if (option & NSKeyValueObservingOptionNew) {
15 value = [self valueForKeyPath:keyPath];
16 if (value == nil) {
17 value = [NSNull null];
18 }
19 }
20 //发送注册通知
21 _NSKeyValueNotifyObserver(observer, keyPath, self, context, value,
22 0 /*originalObservable*/, 1 /*NSKeyValueChangeSetting*/);
23 //加锁
24 pthread_mutex_lock(__NSKeyValueObserverRegistrationLock);
25 }
26 //获取属性的观察信息
27 Info *info = __NSKeyValueRetainedObservationInfoForObject(self, property->_containerClass);
28 //判断是否需要获取新的数值
29 id _additionOriginalObservable = nil;
30 if (option & NSKeyValueObservingOptionNew) {
31 //0x15没有找到定义,猜测为保存是否可观察的数组
32
33 id tsd = _CFGetTSD(0x15);
34 if (tsd != nil) {
35 _additionOriginalObservable = *(tsd + 0x10);
36 }
37 }
38 //在原有信息上生成新的信息
39 Info *newInfo = __NSKeyValueObservationInfoCreateByAdding
40 (info, observer, property, option, context, _additionOriginalObservable, 0, 1);
41 //替换属性的观察信息
42 __NSKeyValueReplaceObservationInfoForObject(self, property->_containerClass, info, newInfo);
43 //属性添加后递归添加关联属性
44 [property object:self didAddObservance:newInfo recurse:true];
45 //获取新的isa
46 Class cls = [property isaForAutonotifying];
47 if ((cls != NULL) && (object_getClass(self) != cls)) {
48 //如果是第一次就替换isa
49 object_setClass(self, cls);
50 }
51 //释放观察信息
52 [newInfo release];
53 if (info != nil) {
54 [info release];
55 }
56 return;
57 }
其中有可能替换方法实现的步骤是获取 isa
的时候,猜测当第一次创建新类的时候,会注册新的方法,接着追踪 isaForAutonotifying
方法。
获取观察类
1 void * -[NSKeyValueUnnestedProperty _isaForAutonotifying]
2 (void * self, void * _cmd) {
3 rbx = self;
4 r14 = *_OBJC_IVAR_$_NSKeyValueProperty._containerClass;
5 if ([*(rbx + r14)->_originalClass
6 automaticallyNotifiesObserversForKey:rbx->_keyPath] != 0x0) {
7 r14 = __NSKeyValueContainerClassGetNotifyingInfo(*(rbx + r14));
8 if (r14 != 0x0) {
9 __NSKVONotifyingEnableForInfoAndKey(r14, rbx->_keyPath);
10 rax = *(r14 + 0x8);
11 }
12 else {
13 rax = 0x0;
14 }
15 }
16 else {
17 rax = 0x0;
18 }
19 return rax;
20 }
立刻发现了熟悉的方法!
automaticallyNotifiesObserversForKey:
是一个类方法,如果你不希望某个属性被观察,那么就设为 NO
,isa
返回是空也就宣告这次添加观察失败。
如果一切顺利的话,将会执行__NSKVONotifyingEnableForInfoAndKey(info, keyPath)
改变 class
的方法,最终返回其 isa
。
实质替换方法
由于该方法实在太长,且使用了goto
不方便阅读,所以依旧整理成伪代码。
1 //伪代码,仅供理解
2 int __NSKVONotifyingEnableForInfoAndKey(void *info, id keyPath) {
3 //线程锁加锁
4 pthread_mutex_lock(info + 0x20);
5 //添加keyPath到数组
6 CFSetAddValue(*(info + 0x10), keyPath);
7 //解锁
8 pthread_mutex_unlock(info + 0x20);
9 //判断原类实现能不能替换
10 Class originClass = *info;
11 MethodClass *methodClass =
12 __NSKeyValueSetterForClassAndKey(originClass, keyPath, originClass);
13 if (![methodClass isKindOfClass:[NSKeyValueMethodSetter class]]) {
14 swizzleMutableMethod(info, keyPath);
15 return;
16 }
17 //判断Setter方法返回值
18 Method method = [methodClass method];
19 if (*(int8_t *)method_getTypeEncoding(method) != _C_VOID) {
20 _NSLog(@"KVO autonotifying only supports -set: methods that return void. ");
21 swizzleMutableMethod(info, keyPath);
22 return;
23 }
24 //获取Setter方法参数
25 char *typeEncoding = method_copyArgumentType(method, 0x2);
26 char type = sign_extend_64(*(int8_t *)typeEncoding);
27 SEL sel;//根据参数类型选择替换的方法
28 switch (type) {
29 case _C_BOOL: sel = __NSSetBoolValueAndNotify;
30 case _C_UCHR: sel = __NSSetUnsignedCharValueAndNotify;
31 case _C_UINT: sel = __NSSetUnsignedIntValueAndNotify;
32 case _C_ULNG: sel = __NSSetUnsignedLongValueAndNotify;
33 case _C_ULNG_LNG: sel = __NSSetUnsignedLongLongValueAndNotify;
34 case _C_CHR: sel = __NSSetCharValueAndNotify;
35 case _C_DBL: sel = __NSSetDoubleValueAndNotify;
36 case _C_FLT: sel = __NSSetFloatValueAndNotify;
37 case _C_INT: sel = __NSSetIntValueAndNotify;
38 case _C_LNG: sel = __NSSetLongValueAndNotify;
39 case _C_LNG_LNG: sel = __NSSetLongLongValueAndNotify;
40 case _C_SHT: sel = __NSSetShortValueAndNotify;
41 case _C_USHT: sel = __NSSetUnsignedShortValueAndNotify;
42 case _C_LNG_LNG: sel = __NSSetLongLongValueAndNotify;
43 case _C_ID: sel = __NSSetObjectValueAndNotify;
44 case "{CGPoint=dd}": sel = __NSSetPointValueAndNotify;
45 case "{_NSRange=QQ}": sel = __NSSetRangeValueAndNotify;
46 case "{CGRect={CGPoint=dd}{CGSize=dd}}": sel = __NSSetRectValueAndNotify;
47 case "{CGSize=dd}": sel = __NSSetSizeValueAndNotify;
48 case *_NSKeyValueOldSizeObjCTypeName: sel = __CF_forwarding_prep_0;
49 default;
50 }
51 //不支持的参数类型打印错误信息
52 if (sel == NULL) {
53 _NSLog(@"KVO autonotifying only supports -set: methods that take id,
54 NSNumber-supported scalar types, and some NSValue-supported structure types.")
55 swizzleMutableMethod(info, keyPath);
56 return;
57 }
58 //替换方法实现
59 SEL methodSel = method_getName(method);
60 _NSKVONotifyingSetMethodImplementation(info, methodSel, sel, keyPath);
61 if (sel == __CF_forwarding_prep_0) {
62 _NSKVONotifyingSetMethodImplementation(info, @selector(forwardInvocation:),
63 _NSKVOForwardInvocation, false);
64 Class cls = *(info + 0x8);
65 SEL newSel = sel_registerName("_original_" + sel_getName(methodSel));
66 Imp imp = method_getImplementation(method);
67 TypeEncoding type = method_getTypeEncoding(method);
68 class_addMethod(cls, newSel, imp, type);
69 }
70 swizzleMutableMethod(info, keyPath);
71 }
可以表述为根据 Setter
方法输入参数类型,匹配合适的 NSSetValueAndNotify
实现来替换,从而实现效果。
那么 swizzleMutableMethod
是干嘛的呢?
1 //替换可变数组集合的方法
2 int swizzleMutableMethod(void *info, id keyPath) {
3 //NSKeyValueArray
4 CFMutableSetRef getterSet = __NSKeyValueMutableArrayGetterForIsaAndKey(*info, keyPath);
5 if ([getterSet respondsToSelector:mutatingMethods]) {
6 mutatingMethods methodList = [getterSet mutatingMethods];
7 replace methodList->insertObjectAtIndex _NSKVOInsertObjectAtIndexAndNotify
8 replace methodList->insertObjectsAtIndexes _NSKVOInsertObjectsAtIndexesAndNotify
9 replace methodList->removeObjectAtIndex _NSKVORemoveObjectAtIndexAndNotify
10 replace methodList->removeObjectsAtIndexes _NSKVORemoveObjectsAtIndexesAndNotify
11 replace methodList->replaceObjectAtIndex _NSKVOReplaceObjectAtIndexAndNotify
12 replace methodList->replaceObjectsAtIndexes _NSKVOReplaceObjectsAtIndexesAndNotify
13 }
14 //NSKeyValueOrderedSet
15 getterSet = __NSKeyValueMutableOrderedSetGetterForIsaAndKey(*info, keyPath);
16 if ([getterSet respondsToSelector:mutatingMethods]) {
17 mutatingMethods methodList = [getterSet mutatingMethods];
18 replace methodList->insertObjectAtIndex _NSKVOInsertObjectAtIndexAndNotify
19 replace methodList->insertObjectsAtIndexes _NSKVOInsertObjectsAtIndexesAndNotify
20 replace methodList->removeObjectAtIndex _NSKVORemoveObjectAtIndexAndNotify
21 replace methodList->removeObjectsAtIndexes _NSKVORemoveObjectsAtIndexesAndNotify
22 replace methodList->replaceObjectAtIndex _NSKVOReplaceObjectAtIndexAndNotify
23 replace methodList->replaceObjectsAtIndexes _NSKVOReplaceObjectsAtIndexesAndNotify
24 }
25 //NSKeyValueSet
26 getterSet = __NSKeyValueMutableSetGetterForClassAndKey(*info, keyPath);
27 if ([getterSet respondsToSelector:mutatingMethods]) {
28 mutatingMethods methodList = [getterSet mutatingMethods];
29 replace methodList->addObject _NSKVOAddObjectAndNotify
30 replace methodList->intersectSet _NSKVOIntersectSetAndNotify
31 replace methodList->minusSet _NSKVOMinusSetAndNotify
32 replace methodList->removeObject _NSKVORemoveObjectAndNotify
33 replace methodList->unionSet _NSKVOUnionSetAndNotify
34 }
35 //改变新类的方法缓存
36 __NSKeyValueInvalidateCachedMutatorsForIsaAndKey(*(info + 0x8), keyPath);
37 return rax;
38 }
前面提到的都是一对一,那如果我想观察一对多的集合类呢?就是通过 KVC
中的 mutableArrayValueForKey:
返回一个代理集合,改变这些代理类的实现做到的。具体的例子之后会介绍。
创建新类
还有一个疑问就是替换的类是怎么创建的?具体方法在 __NSKVONotifyingEnableForInfoAndKey
中实现。
1 //伪代码,仅供理解
2 int __NSKVONotifyingCreateInfoWithOriginalClass(Class cls) {
3 //拼接新名字
4 const char *name = class_getName(cls);
5 int length = strlen(r12) + 0x10;//16是NSKVONotifying_的长度
6 char *newName = malloc(length);
7 __strlcpy_chk(newName, "NSKVONotifying_", length, -1);
8 __strlcat_chk(newName, name, length, -1);
9 //生成一个继承原类的新类
10 Class newCls = objc_allocateClassPair(cls, newName, 0x68);
11 free(newName);
12 if (newCls != NULL) {
13 objc_registerClassPair(newCls);
14 //获取额外的实例变量表
15 void *indexedIvars = object_getIndexedIvars(newCls);
16 *indexedIvars = cls; //记录原isa
17 *(indexedIvars + 0x8) = newCls; //记录新isa
18 //新建一个集合,保存观察的keyPath
19 *(indexedIvars + 0x10) = CFSetCreateMutable(0x0, 0x0, _kCFCopyStringSetCallBacks);
20 //新建一个字典,保存改变过的SEL
21 *(indexedIvars + 0x18) = CFDictionaryCreateMutable(0x0, 0x0, 0x0,
22 _kCFTypeDictionaryValueCallBacks);
23 //新建一个线程锁
24 pthread_mutexattr_init(var_38);
25 pthread_mutexattr_settype(var_38, 0x2);
26 pthread_mutex_init(indexedIvars + 0x20, var_38);
27 pthread_mutexattr_destroy(var_38);
28 //获取NSObject类默认的实现
29 if (*__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectIMPLookupOnce == NULL) {
30 static dispatch_once_t onceToken;
31 dispatch_once(&onceToken, ^{
32 *__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange =
33 class_getMethodImplementation([NSObject class],
34 @selector(willChangeValueForKey:));
35
36 *__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange =
37 class_getMethodImplementation([NSObject class],
38 @selector(didChangeValueForKey:));
39 });
40 }
41 //设置是否替换过ChangeValue方法的flag
42 BOOL isChangedImp = YES;
43 if (class_getMethodImplementation(cls, @selector(willChangeValueForKey:)) ==
44 *__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange) {
45 BOOL isChangedDidImp =
46 class_getMethodImplementation(cls, @selector(didChangeValueForKey:))
47 !=
48 *__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange;
49 isChangedImp = isChangedDidImp ? YES : NO;
50 }
51 *(int8_t *)(indexedIvars + 0x60) = isChangedImp;
52
53 //使用KVO的实现替换原类方法
54 _NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(_isKVOA),
55 _NSKVOIsAutonotifying, false/*是否需要保存SEL到字典*/);
56
57 _NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(dealloc),
58 _NSKVODeallocate, false);
59
60 _NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(class),
61 _NSKVOClass, false);
62 }
63 return newCls;
64 }
建立关系
还有一种情况就是观察的属性依赖于多个关系,比如 color
可能依赖于 r g b a
,其中任何一个改变,都需要通知 color
的变化。
建立关系的方法是
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
或 + (NSSet *)keyPathsForValuesAffecting
返回依赖键值的字符串集合
1 //伪代码
2 + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
3 char *str = "keyPathsForValuesAffecting" + key;
4 SEL sel = sel_registerName(str);
5 Method method = class_getClassMethod(self, sel);
6 if (method != NULL) {
7 result = method_invoke(self, method);
8 } else {
9 result = [self _keysForValuesAffectingValueForKey:key];
10 }
11 return result;
12 }
还记得之前在 _addObserver
方法中有这段代码吗?
//属性添加后递归添加关联属性
[property object:self didAddObservance:newInfo recurse:true];
其中 NSKeyValueProperty
也是一个类簇,具体分为 NSKeyValueProperty NSKeyValueComputedProperty NSKeyValueUnnestedProperty NSKeyValueNestedProperty
,从名字也看出 NSKeyValueNestedProperty
是指嵌套子属性的属性类,那我们观察下他的实现。
1 //伪代码
2 - (void)object:(id)obj didAddObservance:(id)info recurse:(BOOL)isRecurse {
3 if (self->_isAllowedToResultInForwarding != nil) {
4 //获得关系键
5 relateObj = [obj valueForKey:self->_relationshipKey];
6 //注册所有关系通知
7 [relateObj addObserver:info
8 forKeyPath:self->_keyPathFromRelatedObject
9 options:info->options
10 context:nil];
11 }
12 //再往下递归
13 [self->_relationshipProperty object:obj didAddObservance:info recurse:isRecurse];
14 }
至此,实现的大致整体轮廓比较了解了,下面会讲一下怎么把原理运用到实际。
应用原理
手动触发
当 +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
返回是 YES
,那么注册的这个 Key
就会替换对应的 Setter
,从而在改变的时候调用 -(void)willChangeValueForKey:(NSString *)key
与 -(void)didChangeValueForKey:(NSString *)key
发送通知给观察者。
那么只要把自动通知设为 NO
,并代码实现这两个通知方法,就可以达到手动触发的要求。
1 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
2 if ([key isEqualToString:@"object"]) {
3 return false;
4 }
5
6 return [super automaticallyNotifiesObserversForKey:key];
7 }
8
9 - (void)setObject:(NSObject *)object {
10 if (object != _object) {
11 [self willChangeValueForKey:@"object"];
12 _object = object;
13 [self didChangeValueForKey:@"object"];
14 }
15 }
如果操作的是之前提到的集合对象,那么实现的方法就需要变为
1 - (void)willChange:(NSKeyValueChange)changeKind
2 valuesAtIndexes:(NSIndexSet *)indexes
3 forKey:(NSString *)key;
4 - (void)didChange:(NSKeyValueChange)changeKind
5 valuesAtIndexes:(NSIndexSet *)indexes
6 forKey:(NSString *)key;
7
8 - (void)willChangeValueForKey:(NSString *)key
9 withSetMutation:(NSKeyValueSetMutationKind)mutationKind
10 usingObjects:(NSSet *)objects;
11 - (void)didChangeValueForKey:(NSString *)key
12 withSetMutation:(NSKeyValueSetMutationKind)mutationKind
13 usingObjects:(NSSet *)objects;
依赖键观察
之前也有提过构建依赖关系的方法,具体操作如下
1 + (NSSet*)keyPathsForValuesAffectingValueForKey:(NSString *)key { 2 if ([key isEqualToString:@"color"]) { 3 return [NSSet setWithObjects:@"r",@"g",@"b",@"a",nil]; 4 } 5 6 return [super keyPathsForValuesAffectingValueForKey:key]; 7 } 8 9 //建议使用静态指针地址作为上下文区分不同的观察 10 static void * const kColorContext = (void*)&kColorContext; 11 - (void)viewDidLoad { 12 [super viewDidLoad]; 13 14 [self addObserver:self forKeyPath:@"color" 15 options:NSKeyValueObservingOptionNew 16 context:kColorContext]; 17 self.r = 133; 18 } 19 20 - (void)observeValueForKeyPath:(NSString *)keyPath 21 ofObject:(id)object 22 change:(NSDictionary > *)change 23 context:(void *)context { 24 if (context == kColorContext) { 25 NSLog(@"%@", keyPath); 26 //outprint --> color 27 } 28 }id
可变数组与集合
不可变的数组与集合由于内部结构固定,所以只能通过观察容器类内存地址来判断是否变化,也就是 NSKeyValueChangeSetting
。
集合和数组的观察都很类似,我们先关注如果要观察可变数组内部插入移除的变化呢?
先了解一下集合代理方法,- (NSMutableArray *)mutableArrayValueForKey:
,这是一个 KVC
方法,能够返回一个可供观察的 NSKeyValueArray
对象。
根据苹果注释,其搜索顺序如下
1.搜索是否实现最少一个插入与一个删除方法
1 -insertObject:inAtIndex:
2 -removeObjectFromAtIndex:
3 -insert:atIndexes:
4 -removeAtIndexes:
2.否则搜索是否有 set
方法,有的话每次都把修改数组重新赋值回原属性。
3.否则检查 + (BOOL)accessInstanceVariablesDirectly
,如果是YES
,就查找成员变量_
,此后所有的操作针对代理都转接给成员变量执行。
4.最后进入保护方法valueForUndefinedKey:
第一种方法
1 - (void)insertObject:(NSObject *)object inDataArrayAtIndex:(NSUInteger)index {
2 [_dataArray insertObject:object atIndex:index];
3 }
4
5 - (void)removeObjectFromDataArrayAtIndex:(NSUInteger)index {
6 [_dataArray removeObjectAtIndex:index];
7 }
8
9 - (void)viewDidLoad {
10 [super viewDidLoad];
11
12 _dataArray = @[].mutableCopy;
13 [self addObserver:self forKeyPath:@"dataArray"
14 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld |
15 NSKeyValueObservingOptionPrior context:nil];
16 [self insertObject:@1 inDataArrayAtIndex:0];
17 }
通过实现了insert
与remove
方法,使得代理数组能够正常运作数组变量,KVO
观察了代理数组的这两个方法,发出了我们需要的通知。
这种方式使用了第一步搜索,比较容易理解,缺点是改动的代码比较多,改动数组必须通过自定义方法。
第二种方法
1 @property (nonatomic, strong, readonly) NSMutableArray *dataArray;
2
3 @synthesize dataArray = _dataArray;
4
5 - (NSMutableArray *)dataArray {
6 return [self mutableArrayValueForKey:@"dataArray"];
7 }
8
9 - (void)viewDidLoad {
10 [super viewDidLoad];
11
12 _dataArray = @[].mutableCopy;
13 [self addObserver:self forKeyPath:@"dataArray"
14 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld |
15 NSKeyValueObservingOptionPrior context:nil];
16 [self.dataArray addObject:@1];
17 }
这种方式相对来说更简洁,修改数组的方法与平时一致,比较适合使用。
下面说一下原理,首先我们没有实现对应的insert
与remove
方法,其次readonly
属性也没有set
方法,但我们实现了 @synthesize dataArray = _dataArray;
所以根据第三步对代理数组的操作都会实际操作到实例变量中。
然后重载了 dataArray
的 Getter
方法,保证了修改数组时必须调用主体是self.dataArray
,也就是代理数组,从而发送通知。
问答
KVO的底层实现?
KVO
就是通过 Runtime
替换被观察类的 Setter
实现,从而在发生改变时发起通知。
如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?
通过设置 automaticallyNotifiesObserversForKey
为 False
实现取消自动触发。
符合条件再触发可以这么实现。
1 - (void)setObject:(NSObject *)object {
2 if (object == _object) return;
3
4 BOOL needNotify = [object isKindOfClass:[NSString class]];
5 if (needNotify) {
6 [self willChangeValueForKey:@"object"];
7 }
8 _object = object;
9 if (needNotify) {
10 [self didChangeValueForKey:@"object"];
11 }
12 }
总结
由于对汇编语言、反编译工具、objc4
开源代码的不熟悉,这篇文章写了一周时间,结构也有点混乱。
所幸还是理顺了整体结构,在整理的过程中学会了很多很多。
由于才疏学浅,其中对汇编和源码的解释难免出错,还望大佬多多指教!
资料分享
ObjC中国的期刊 KVC和KVO
杨大牛的 Objective-C中的KVC和KVO
iOS开发技巧系列---详解KVC(我告诉你KVC的一切)