介绍
KVO
全称KeyValueObserving
,是苹果提供的一套事件告诉机制。容许对象监听另一个对象特定属性的扭转,并在扭转时接管到事件。因为KVO
的实现机制,所以对属性才会产生作用,个别继承自NSObject
的对象都默认反对KVO
。
KVO
和NSNotificationCenter
都是iOS
中观察者模式的一种实现。区别在于,绝对于被观察者和观察者之间的关系,KVO
是一对一的,而不一对多的。KVO
对被监听对象无侵入性,不须要手动批改其外部代码即可实现监听。
KVO
能够监听单个属性的变动,也能够监听汇合对象的变动。通过KVC
的mutableArrayValueForKey:
等办法取得代理对象,当代理对象的外部对象产生扭转时,会回调KVO
监听的办法。汇合对象蕴含NSArray
和NSSet
。
应用
应用KVO
分为三个步骤
- 通过
addObserver:forKeyPath:options:context:
办法注册观察者,观察者能够接管keyPath
属性的变动事件回调。 - 在观察者中实现
observeValueForKeyPath:ofObject:change:context:
办法,当keyPath
属性产生扭转后,KVO
会回调这个办法来告诉观察者。 - 当观察者不须要监听时,能够调用
removeObserver:forKeyPath:
办法将KVO
移除。须要留神的是,调用removeObserver
须要在观察者隐没之前,否则会导致Crash
。
注册
在注册观察者时,能够传入options
参数,参数是一个枚举类型。如果传入NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
示意接管新值和旧值,默认为只接管新值。如果想在注册观察者后,立刻接管一次回调,则能够退出NSKeyValueObservingOptionInitial
枚举。
还能够通过办法context
传入任意类型的对象,在接管音讯回调的代码中能够接管到这个对象,是KVO
中的一种传值形式。
在调用addObserver
办法后,KVO
并不会对观察者进行强援用。所以须要留神观察者的生命周期,否则会导致观察者被开释带来的Crash
。
监听
观察者须要实现observeValueForKeyPath:ofObject:change:context:
办法,当KVO
事件到来时会调用这个办法,如果没有实现会导致Crash
。change
字典中寄存KVO
属性相干的值,依据options
时传入的枚举来返回。枚举会对应相应key
来从字典中取出值,例如有NSKeyValueChangeOldKey
字段,存储扭转之前的旧值。
change
中还有NSKeyValueChangeKindKey
字段,和NSKeyValueChangeOldKey
是平级的关系,来提供本次更改的信息,对应NSKeyValueChange
枚举类型的value
。例如被察看属性产生扭转时,字段为NSKeyValueChangeSetting
。
如果被察看对象是汇合对象,在NSKeyValueChangeKindKey
字段中会蕴含NSKeyValueChangeInsertion
、NSKeyValueChangeRemoval
、NSKeyValueChangeReplacement
的信息,示意汇合对象的操作形式。
其余触发办法
调用KVO
属性对象时,不仅能够通过点语法和set
语法进行调用,KVO
兼容很多种调用形式。
// 间接调用set办法,或者通过属性的点语法间接调用[account setName:@"Savings"];// 应用KVC的setValue:forKey:办法[account setValue:@"Savings" forKey:@"name"];// 应用KVC的setValue:forKeyPath:办法[document setValue:@"Savings" forKeyPath:@"account.name"];// 通过mutableArrayValueForKey:办法获取到代理对象,并应用代理对象进行操作Transaction *newTransaction = <#Create a new transaction for the account#>;NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];[transactions addObject:newTransaction];
理论利用
KVO
次要用来做键值察看操作,想要一个值产生扭转后告诉另一个对象,则用KVO
实现最为适合。斯坦福大学的iOS
教程中有一个很经典的案例,通过KVO
在Model
和Controller
之间进行通信。
触发
被动触发
KVO
在属性产生扭转时的调用是主动的,如果想要手动管制这个调用机会,或想本人实现KVO
属性的调用,则能够通过KVO
提供的办法进行调用。
- (void)setBalance:(double)theBalance { if (theBalance != _balance) { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self didChangeValueForKey:@"balance"]; }}
能够看到调用KVO
次要依附两个办法,在属性产生扭转之前调用willChangeValueForKey:
办法,在产生扭转之后调用didChangeValueForKey:
办法。然而,如果不调用willChangeValueForKey
,间接调用didChangeValueForKey
是不失效的,二者有先后顺序并且须要成对呈现。
禁用KVO
如果想禁止某个属性的KVO
,例如要害信息不想被三方SDK
通过KVO
的形式获取,能够通过automaticallyNotifiesObserversForKey
办法返回NO
来禁止其余中央对这个属性进行KVO
。办法返回YES
则示意能够调用,如果返回NO
则示意不能够调用。此办法是一个类办法,能够在办法外部判断keyPath
,来抉择这个属性是否容许被KVO
。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"balance"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic;}
KVC触发
KVC
对KVO
有非凡兼容,当通过KVC
调用非属性的实例变量时,KVC
外部也会触发KVO
的回调,并通过NSKeyValueDidChange
和NSKeyValueWillChange
向上回调。
上面疏忽main
函数向上的零碎函数,只保留要害堆栈。这是通过调用属性setter
办法的形式回调的KVO
堆栈。
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 38.1* frame #0: 0x0000000101bc3a15 TestKVO`::-[ViewController observeValueForKeyPath:ofObject:change:context:](self=0x00007f8419705890, _cmd="observeValueForKeyPath:ofObject:change:context:", keyPath="object", object=0x0000604000015b00, change=0x0000608000265540, context=0x0000000000000000) at ViewController.mm:84frame #1: 0x000000010327e820 Foundation`NSKeyValueNotifyObserver + 349frame #2: 0x000000010327e0d7 Foundation`NSKeyValueDidChange + 483frame #3: 0x000000010335f22b Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:] + 778frame #4: 0x000000010324b1b4 Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 61frame #5: 0x00000001032a7b79 Foundation`_NSSetObjectValueAndNotify + 255frame #6: 0x0000000101bc3937 TestKVO`::-[ViewController viewDidLoad](self=0x00007f8419705890, _cmd="viewDidLoad") at ViewController.mm:70
这是通过KVC
触发的向上回调,能够看到失常通过批改属性的形式触发KVO
,和通过KVC
触发的KVO
还是有区别的。通过KVC
的形式触发KVO
,甚至都没有_NSSetObjectValueAndNotify
的调用。
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 37.1* frame #0: 0x0000000106be1a85 TestKVO`::-[ViewController observeValueForKeyPath:ofObject:change:context:](self=0x00007fe68ac07710, _cmd="observeValueForKeyPath:ofObject:change:context:", keyPath="object", object=0x0000600000010c80, change=0x000060c000262780, context=0x0000000000000000) at ViewController.mm:84frame #1: 0x000000010886d820 Foundation`NSKeyValueNotifyObserver + 349frame #2: 0x000000010886d0d7 Foundation`NSKeyValueDidChange + 483frame #3: 0x000000010894d422 Foundation`NSKeyValueDidChangeWithPerThreadPendingNotifications + 148frame #4: 0x0000000108879b47 Foundation`-[NSObject(NSKeyValueCoding) setValue:forKey:] + 292frame #5: 0x0000000106be19aa TestKVO`::-[ViewController viewDidLoad](self=0x00007fe68ac07710, _cmd="viewDidLoad") at ViewController.mm:70
实现原理
外围逻辑
KVO
是通过isa-swizzling
技术实现的,这是整个KVO
实现的重点。在运行时依据原类创立一个两头类,这个两头类是原类的子类,并动静批改以后对象的isa
指向两头类。并且将class
办法重写,返回原类的Class
。苹果重写class
办法,就是为了屏蔽两头类的存在。
所以,苹果倡议在开发中不应该依赖isa
指针,而是通过class
实例办法来获取对象类型,来防止被KVO
或者其余runtime
办法影响。
_NSSetObjectValueAndNotify
随后会批改两头类对应的set
办法,并且插入willChangeValueForkey
办法以及didChangeValueForKey
办法,在两个办法两头调用父类的set
办法。这个过程,零碎将其封装到_NSSetObjectValueAndNotify
函数中。通过查看这个函数的汇编代码,能够看到外部封装的willChangeValueForkey
办法和didChangeValueForKey
办法的调用。
零碎并不是只封装了_NSSetObjectValueAndNotify
函数,而是会依据属性类型,调用不同的函数。如果是Int
类型就会调用_NSSetIntValueAndNotify
,这些实现都定义在Foundation
框架中。具体的能够通过hopper
来查看Foundation
框架的实现。
runtime
会将新生成的NSKVONotifying_KVOTest
的setObject
办法的实现,替换成_NSSetObjectValueAndNotify
函数,而不是重写setObject
函数。通过上面的测试代码,能够查看selector
对应的IMP
,并且将其实现的地址打印进去。
KVOTest *test = [[KVOTest alloc] init];[test setObject:[[NSObject alloc] init]];NSLog(@"%p", [test methodForSelector:@selector(setObject:)]);[test addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];[test setObject:[[NSObject alloc] init]];NSLog(@"%p", [test methodForSelector:@selector(setObject:)]);// 打印后果,第一次的办法地址为0x100c8e270,第二次的办法地址为0x7fff207a3203(lldb) p (IMP)0x100c8e270(IMP) $0 = 0x0000000100c8e270 (DemoProject`-[KVOTest setObject:] at KVOTest.h:11)(lldb) p (IMP)0x7fff207a3203(IMP) $1 = 0x00007fff207a3203 (Foundation`_NSSetObjectValueAndNotify)
_NSKVONotifyingCreateInfoWithOriginalClass
对于零碎实现KVO
的原理,能够对object_setClass
打断点,或者对objc_allocateClassPair
办法打断点也能够,这两个办法都是创立类必走的办法。通过这两个办法的汇编堆栈,向前回溯。随后,能够失去翻译后如下的汇编代码。
能够看到有一些类名拼接规定,随后依据类名创立新类。如果newCls
为空则曾经创立过,或者可能为空。如果newCls
不为空,则注册新创建的类,并且设置SDTestKVOClassIndexedIvars
构造体的一些参数。
Class _NSKVONotifyingCreateInfoWithOriginalClass(Class originalClass) { const char *clsName = class_getName(originalClass); size_t len = strlen(clsName); len += 0x10; char *newClsName = malloc(len); const char *prefix = "NSKVONotifying_"; __strlcpy_chk(newClsName, prefix, len); __strlcat_chk(newClsName, clsName, len, -1); Class newCls = objc_allocateClassPair(originalClass, newClsName, 0x68); if (newCls) { objc_registerClassPair(newCls); SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(newCls); indexedIvars->originalClass = originalClass; indexedIvars->KVOClass = newCls; CFMutableSetRef mset = CFSetCreateMutable(nil, 0, kCFCopyStringSetCallBacks); indexedIvars->mset = mset; CFMutableDictionaryRef mdict = CFDictionaryCreateMutable(nil, 0, nil, kCFTypeDictionaryValueCallBacks); indexedIvars->mdict = mdict; pthread_mutex_init(indexedIvars->lock); static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ bool flag = true; IMP willChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, @selector(willChangeValueForKey:)); IMP didChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, @selector(didChangeValueForKey:)); if (willChangeValueForKeyImp == _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange && didChangeValueForKeyImp == _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange) { flag = false; } indexedIvars->flag = flag; NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(_isKVOA), NSKVOIsAutonotifying, nil); NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(dealloc), NSKVODeallocate, nil); NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(class), NSKVOClass, nil); }); } else { return nil; } return newCls;}
验证
为了验证KVO
的实现形式,咱们退出上面的测试代码。首先创立一个KVOObject
类,并在外面退出两个属性,而后重写description
办法,并在外部打印一些要害参数。
须要留神的是,为了验证KVO
在运行时做了什么,我打印了对象的class
办法,以及通过runtime
获取对象的类和父类。在增加KVO
监听前后,都打印一次,察看零碎做了什么。
@interface KVOObject : NSObject@property (nonatomic, copy ) NSString *name;@property (nonatomic, assign) NSInteger age;@end- (NSString *)description { IMP nameIMP = class_getMethodImplementation(object_getClass(self), @selector(setName:)); IMP ageIMP = class_getMethodImplementation(object_getClass(self), @selector(setAge:)); NSLog(@"object setName: IMP %p object setAge: IMP %p \n", nameIMP, ageIMP); Class objectMethodClass = [self class]; Class objectRuntimeClass = object_getClass(self); Class superClass = class_getSuperclass(objectRuntimeClass); NSLog(@"objectMethodClass : %@, ObjectRuntimeClass : %@, superClass : %@ \n", objectMethodClass, objectRuntimeClass, superClass); NSLog(@"object method list \n"); unsigned int count; Method *methodList = class_copyMethodList(objectRuntimeClass, &count); for (NSInteger i = 0; i < count; i++) { Method method = methodList[i]; NSString *methodName = NSStringFromSelector(method_getName(method)); NSLog(@"method Name = %@\n", methodName); } return @"";}
创立一个KVOObject
对象,在KVO
前后别离打印对象的要害信息,看KVO
前后有什么变动。
self.object = [[KVOObject alloc] init];[self.object description];[self.object addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];[self.object description];
上面是KVO
前后打印的要害信息。
咱们发现对象被KVO
后,其真正类型变为了NSKVONotifying_KVOObject
类,曾经不是之前的类了。KVO
会在运行时动态创建一个新类,将对象的isa
指向新创建的类,并且将superClass
指向原来的类KVOObject
,新创建的类命名规定是NSKVONotifying_xxx
的格局。KVO
为了使其更像之前的类,还会将对象的class
实例办法重写,使其更像原类。
增加KVO
之后,因为批改了setName
办法和setAge
办法的IMP
,所以打印这两个办法的IMP
,也是一个新的地址,新的实现在NSKVONotifying_KVOObject
中。
这种实现形式对业务代码没有侵入性,能够在不影响KVOObject
其余对象的前提下,对单个对象进行监听并批改其办法实现,在赋值时触发KVO
回调。
在下面的代码中还发现了_isKVOA
办法,这个办法能够当做应用了KVO
的一个标记,零碎可能也是这么用的。如果咱们想判断以后类是否是KVO
动静生成的类,就能够从办法列表中搜寻这个办法。
// 第一次object address : 0x604000239340object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObjectobject method listmethod Name = .cxx_destructmethod Name = descriptionmethod Name = namemethod Name = setName:method Name = setAge:method Name = age// 第二次object address : 0x604000239340object setName: IMP 0x10ea8defe object setAge: IMP 0x10ea94106objectMethodClass : KVOObject, ObjectRuntimeClass : NSKVONotifying_KVOObject, superClass : KVOObjectobject method listmethod Name = setAge:method Name = setName:method Name = classmethod Name = deallocmethod Name = _isKVOA
object_getClass
为什么下面调用runtime
的object_getClass
函数,就能够获取到真正的类呢?
调用object_getClass
函数后其返回的是一个Class
类型,Class
是objc_class
定义的一个typedef
别名,通过objc_class
就能够获取到对象的isa
指针指向的Class
,也就是对象的类对象。
由此能够晓得,object_getClass
函数外部返回的是对象的isa
指针。
typedef struct objc_class *Class;struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY;#if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;#endif}
留神点
Crash
KVO
的addObserver
和removeObserver
须要是成对的,如果反复remove
则会导致NSRangeException
类型的Crash
,如果遗记remove
则会在观察者开释后再次接管到KVO
回调时Crash
。
苹果官网举荐的形式是,在init
的时候进行addObserver
,在dealloc
时removeObserver
,这样能够保障add
和remove
是成对呈现的,是一种比拟现实的应用形式。
谬误查看
如果传入一个谬误的keyPath
并不会有谬误提醒。在调用KVO
时须要传入一个keyPath
,因为keyPath
是字符串的模式,如果属性名产生扭转后,字符串没有扭转容易导致Crash
。对于这个问题,咱们能够利用零碎的反射机制将keyPath
反射进去,这样编译器能够在@selector()
中进行合法性检查。
NSString *keyPath = NSStringFromSelector(@selector(isFinished));
不能触发回调
因为KVO
的实现机制,如果调用成员变量进行赋值,是不会触发KVO
的。
@interface TestObject : NSObject { @public NSObject *object;}@end// 谬误的调用形式self.object = [[TestObject alloc] init];[self.object addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];self.object->object = [[NSObject alloc] init];
然而,如果通过KVC
的形式调用赋值操作,则会触发KVO
的回调办法。这是因为KVC
对KVO
有独自的兼容,在KVC
的赋值办法外部,手动调用了willChangeValueForKey:
和didChangeValueForKey:
办法。
// KVC的形式调用self.object = [[TestObject alloc] init];[self.object addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];[self.object setValue:[[NSObject alloc] init] forKey:@"object"];
反复增加
对KVO
进行反复addObserver
并不会导致解体,然而会呈现反复执行KVO
回调办法的问题。
[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];self.testLabel.text = @"test";[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];self.testLabel.text = @"test";// 输入2018-08-03 11:48:49.502450+0800 KVOTest[5846:412257] test2018-08-03 11:48:52.975102+0800 KVOTest[5846:412257] test2018-08-03 11:48:53.547145+0800 KVOTest[5846:412257] test2018-08-03 11:48:54.087171+0800 KVOTest[5846:412257] test2018-08-03 11:48:54.649244+0800 KVOTest[5846:412257] test
通过下面的测试代码,并且在回调中打印object
所对应的Class
来看,并不会反复创立子类,始终都是一个类。尽管反复addobserver
不会立即解体,然而反复增加后在第一次调用removeObserver
时,就会立即解体。从解体堆栈来看,和反复移除的问题一样,都是零碎被动抛出的异样。
Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <UILabel 0x7f859b547490> for the key path "text" from <UILabel 0x7f859b547490> because it is not registered as an observer.'
反复移除
KVO
是不容许对一个keyPath
进行反复移除的,如果反复移除,则会导致解体。例如上面的测试代码。
[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];self.testLabel.text = @"test";[self.testLabel removeObserver:self forKeyPath:@"text"];[self.testLabel removeObserver:self forKeyPath:@"text"];[self.testLabel removeObserver:self forKeyPath:@"text"];
执行下面的测试代码后,会造成上面的解体信息。从KVO
的解体堆栈能够看进去,零碎为了实现KVO
的addObserver
和removeObserver
,为NSObject
增加了一个名为NSKeyValueObserverRegistration
的Category
,KVO
的addObserver
和removeObserver
的实现都在外面。
在移除KVO
的监听时,零碎会判断以后KVO
的keyPath
是否曾经被移除,如果曾经被移除,则被动抛出一个NSException
的异样。
2018-08-03 10:54:27.477379+0800 KVOTest[4939:286991] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <ViewController 0x7ff6aee31600> for the key path "text" from <UILabel 0x7ff6aee2e850> because it is not registered as an observer.'*** First throw call stack:( 0 CoreFoundation 0x000000010db2312b __exceptionPreprocess + 171 1 libobjc.A.dylib 0x000000010cc6af41 objc_exception_throw + 48 2 CoreFoundation 0x000000010db98245 +[NSException raise:format:] + 197 3 Foundation 0x0000000108631f15 -[NSObject(NSKeyValueObserverRegistration) _removeObserver:forProperty:] + 497 4 Foundation 0x0000000108631ccb -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:] + 84 5 KVOTest 0x0000000107959a55 -[ViewController viewDidAppear:] + 373 // ..... 20 UIKit 0x000000010996d5d6 UIApplicationMain + 159 21 KVOTest 0x00000001079696cf main + 111 22 libdyld.dylib 0x000000010fb43d81 start + 1)libc++abi.dylib: terminating with uncaught exception of type NSException
排查链路
KVO
是一种事件绑定机制的实现,在keyPath
对应的值产生扭转后会回调对应的办法。这种数据绑定机制,在对象关系很简单的状况下,很容易导致不好排查的bug
。例如keyPath
对应的属性被调用的关系很简单,就不太倡议对这个属性进行KVO
。
本人实现KVO
除了下面的毛病,KVO
还不反对block
语法,须要独自重写父类办法,这样加上add
和remove
办法就会导致代码很扩散。所以,我通过runtime
简略的实现了一个KVO
,源码放在我的Github
上,叫做EasyKVO。
self.object = [[KVOObject alloc] init];[self.object lxz_addObserver:self originalSelector:@selector(name) callback:^(id observedObject, NSString *observedKey, id oldValue, id newValue) { // 解决业务逻辑}];self.object.name = @"lxz";// 移除告诉[self.object lxz_removeObserver:self originalSelector:@selector(name)];
调用代码很简略,间接通过lxz_addObserver:originalSelector:callback:
办法就能够增加KVO
的监听,能够通过callback
的block
接管属性产生扭转后的回调。而且办法的keyPath
接管的是一个SEL
类型参数,所以能够通过@selector()
传入参数时进行办法合法性检查,如果是未实现的办法间接就会报正告。
通过lxz_removeObserver:originalSelector:
办法传入观察者和keyPath
,当观察者所有keyPath
都移除后则从KVO
中移除观察者对象。
如果反复addObserver
和removeObserver
也没事,外部有判断逻辑。EasyKVO
外部通过weak
对观察者做援用,并不会影响观察者的生命周期,并且在观察者开释后不会导致Crash
。一次add
办法调用对应一个block
,如果观察者监听多个keyPath
属性,不须要在block
回调中判断keyPath
。
KVOController
想在我的项目中平安便捷的应用KVO
的话,举荐Facebook
的一个KVO
开源第三方框架KVOController。KVOController
实质上是对系统KVO
的封装,具备原生KVO
所有的性能,而且躲避了原生KVO
的很多问题,兼容block
和action
两种回调形式。
源码剖析
从源码来看还是比较简单的,次要分为NSObject
的Category
和FBKVOController
两局部。
在Category
中提供了KVOController
和KVOControllerNonRetaining
两个属性,顾名思义第一个会对observer
产生强援用,第二个则不会。其外部代码就是创立FBKVOController
对象的代码,并将创立进去的对象赋值给Category
的属性,间接通过这个Category
就能够懒加载创立FBKVOController
对象。
- (FBKVOController *)KVOControllerNonRetaining{ id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey); if (nil == controller) { controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO]; self.KVOControllerNonRetaining = controller; } return controller;}
实现原理
在FBKVOController
中分为三局部,_FBKVOInfo
是一个公有类,这个类的性能很简略,就是以结构化的模式保留FBKVOController
所需的各个对象,相似于模型类的性能。
还有一个公有类_FBKVOSharedController
,这是FBKVOController
框架实现的要害。从命名上能够看出其是一个单例,所有通过FBKVOController
实现的KVO
,观察者都是它。每次通过FBKVOController
增加一个KVO
时,_FBKVOSharedController
都会将本人设为观察者,并在其外部实现observeValueForKeyPath:ofObject:change:context:
办法,将接管到的音讯通过block
或action
进行转发。
其性能很简略,通过observe:info:
办法增加KVO
监听,并用一个NSHashTable
保留_FBKVOInfo
信息。通过unobserve:info:
办法移除监听,并从NSHashTable
中将对应的_FBKVOInfo
移除。这两个办法外部都会调用零碎的KVO
办法。
在外界应用时须要用FBKVOController
类,其外部实现了初始化以及增加和移除监听的操作。在调用增加监听办法后,其外部会创立一个_FBKVOInfo
对象,并通过一个NSMapTable
对象进行持有,而后会调用_FBKVOSharedController
来进行注册监听。
应用FBKVOController
的话,不须要手动调用removeObserver
办法,在被监听对象隐没的时候,会在dealloc
中调用remove
办法。如果因为业务需要,能够手动调用remove
办法,反复调用remove
办法不会有问题。
- (void)_observe:(id)object info:(_FBKVOInfo *)info{ NSMutableSet *infos = [_objectInfosMap objectForKey:object]; _FBKVOInfo *existingInfo = [infos member:info]; if (nil != existingInfo) { return; } if (nil == infos) { infos = [NSMutableSet set]; [_objectInfosMap setObject:infos forKey:object]; } [infos addObject:info]; [[_FBKVOSharedController sharedController] observe:object info:info];}
因为FBKVOController
的实现很简略,所以这里就很简略的讲讲,具体实现能够去Github下载源码仔细分析一下。