該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯
介紹
KVO
全稱KeyValueObserving
竭宰,是蘋果提供的一套事件通知機(jī)制。允許對象監(jiān)聽另一個對象特定屬性的改變留量,并在改變時接收到事件吼具。由于KVO
的實現(xiàn)機(jī)制,所以對屬性才會發(fā)生作用从橘,一般繼承自NSObject
的對象都默認(rèn)支持KVO
念赶。
KVO
和NSNotificationCenter
都是iOS
中觀察者模式的一種實現(xiàn)。區(qū)別在于恰力,相對于被觀察者和觀察者之間的關(guān)系叉谜,KVO
是一對一的,而不一對多的踩萎。KVO
對被監(jiān)聽對象無侵入性停局,不需要手動修改其內(nèi)部代碼即可實現(xiàn)監(jiān)聽。
KVO
可以監(jiān)聽單個屬性的變化香府,也可以監(jiān)聽集合對象的變化董栽。通過KVC
的mutableArrayValueForKey:
等方法獲得代理對象,當(dāng)代理對象的內(nèi)部對象發(fā)生改變時企孩,會回調(diào)KVO
監(jiān)聽的方法锭碳。集合對象包含NSArray
和NSSet
。
使用
使用KVO
分為三個步驟
- 通過
addObserver:forKeyPath:options:context:
方法注冊觀察者勿璃,觀察者可以接收keyPath
屬性的變化事件回調(diào)擒抛。 - 在觀察者中實現(xiàn)
observeValueForKeyPath:ofObject:change:context:
方法,當(dāng)keyPath
屬性發(fā)生改變后补疑,KVO
會回調(diào)這個方法來通知觀察者闻葵。 - 當(dāng)觀察者不需要監(jiān)聽時,可以調(diào)用
removeObserver:forKeyPath:
方法將KVO
移除癣丧。需要注意的是槽畔,調(diào)用removeObserver
需要在觀察者消失之前,否則會導(dǎo)致Crash
胁编。
注冊
在注冊觀察者時厢钧,可以傳入options
參數(shù)鳞尔,參數(shù)是一個枚舉類型。如果傳入NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
表示接收新值和舊值早直,默認(rèn)為只接收新值寥假。如果想在注冊觀察者后,立即接收一次回調(diào)霞扬,則可以加入NSKeyValueObservingOptionInitial
枚舉糕韧。
還可以通過方法context
傳入任意類型的對象,在接收消息回調(diào)的代碼中可以接收到這個對象喻圃,是KVO
中的一種傳值方式萤彩。
在調(diào)用addObserver
方法后,KVO
并不會對觀察者進(jìn)行強(qiáng)引用斧拍。所以需要注意觀察者的生命周期雀扶,否則會導(dǎo)致觀察者被釋放帶來的Crash
。
監(jiān)聽
觀察者需要實現(xiàn)observeValueForKeyPath:ofObject:change:context:
方法肆汹,當(dāng)KVO
事件到來時會調(diào)用這個方法愚墓,如果沒有實現(xiàn)會導(dǎo)致Crash
。change
字典中存放KVO
屬性相關(guān)的值昂勉,根據(jù)options
時傳入的枚舉來返回浪册。枚舉會對應(yīng)相應(yīng)key
來從字典中取出值,例如有NSKeyValueChangeOldKey
字段岗照,存儲改變之前的舊值村象。
change
中還有NSKeyValueChangeKindKey
字段,和NSKeyValueChangeOldKey
是平級的關(guān)系谴返,來提供本次更改的信息,對應(yīng)NSKeyValueChange
枚舉類型的value
咧织。例如被觀察屬性發(fā)生改變時嗓袱,字段為NSKeyValueChangeSetting
。
如果被觀察對象是集合對象习绢,在NSKeyValueChangeKindKey
字段中會包含NSKeyValueChangeInsertion
渠抹、NSKeyValueChangeRemoval
、NSKeyValueChangeReplacement
的信息闪萄,表示集合對象的操作方式梧却。
其他觸發(fā)方法
調(diào)用KVO
屬性對象時,不僅可以通過點語法和set
語法進(jìn)行調(diào)用败去,KVO
兼容很多種調(diào)用方式放航。
// 直接調(diào)用set方法,或者通過屬性的點語法間接調(diào)用
[account setName:@"Savings"];
// 使用KVC的setValue:forKey:方法
[account setValue:@"Savings" forKey:@"name"];
// 使用KVC的setValue:forKeyPath:方法
[document setValue:@"Savings" forKeyPath:@"account.name"];
// 通過mutableArrayValueForKey:方法獲取到代理對象圆裕,并使用代理對象進(jìn)行操作
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
實際應(yīng)用
KVO
主要用來做鍵值觀察操作广鳍,想要一個值發(fā)生改變后通知另一個對象荆几,則用KVO
實現(xiàn)最為合適。斯坦福大學(xué)的iOS
教程中有一個很經(jīng)典的案例赊时,通過KVO
在Model
和Controller
之間進(jìn)行通信吨铸。
觸發(fā)
主動觸發(fā)
KVO
在屬性發(fā)生改變時的調(diào)用是自動的,如果想要手動控制這個調(diào)用時機(jī)祖秒,或想自己實現(xiàn)KVO
屬性的調(diào)用诞吱,則可以通過KVO
提供的方法進(jìn)行調(diào)用。
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
可以看到調(diào)用KVO
主要依靠兩個方法竭缝,在屬性發(fā)生改變之前調(diào)用willChangeValueForKey:
方法房维,在發(fā)生改變之后調(diào)用didChangeValueForKey:
方法。但是歌馍,如果不調(diào)用willChangeValueForKey
握巢,直接調(diào)用didChangeValueForKey
是不生效的,二者有先后順序并且需要成對出現(xiàn)松却。
禁用KVO
如果想禁止某個屬性的KVO
暴浦,例如關(guān)鍵信息不想被三方SDK
通過KVO
的方式獲取,可以通過automaticallyNotifiesObserversForKey
方法返回NO
來禁止其他地方對這個屬性進(jìn)行KVO
晓锻。方法返回YES
則表示可以調(diào)用歌焦,如果返回NO
則表示不可以調(diào)用。此方法是一個類方法砚哆,可以在方法內(nèi)部判斷keyPath
独撇,來選擇這個屬性是否允許被KVO
。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
KVC觸發(fā)
KVC
對KVO
有特殊兼容躁锁,當(dāng)通過KVC
調(diào)用非屬性的實例變量時纷铣,KVC
內(nèi)部也會觸發(fā)KVO
的回調(diào),并通過NSKeyValueDidChange
和NSKeyValueWillChange
向上回調(diào)战转。
下面忽略main
函數(shù)向上的系統(tǒng)函數(shù)搜立,只保留關(guān)鍵堆棧。這是通過調(diào)用屬性setter
方法的方式回調(diào)的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:84
frame #1: 0x000000010327e820 Foundation`NSKeyValueNotifyObserver + 349
frame #2: 0x000000010327e0d7 Foundation`NSKeyValueDidChange + 483
frame #3: 0x000000010335f22b Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:] + 778
frame #4: 0x000000010324b1b4 Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 61
frame #5: 0x00000001032a7b79 Foundation`_NSSetObjectValueAndNotify + 255
frame #6: 0x0000000101bc3937 TestKVO`::-[ViewController viewDidLoad](self=0x00007f8419705890, _cmd="viewDidLoad") at ViewController.mm:70
這是通過KVC
觸發(fā)的向上回調(diào)啄踊,可以看到正常通過修改屬性的方式觸發(fā)KVO
,和通過KVC
觸發(fā)的KVO
還是有區(qū)別的刁标。通過KVC
的方式觸發(fā)KVO
颠通,甚至都沒有_NSSetObjectValueAndNotify
的調(diào)用。
* 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:84
frame #1: 0x000000010886d820 Foundation`NSKeyValueNotifyObserver + 349
frame #2: 0x000000010886d0d7 Foundation`NSKeyValueDidChange + 483
frame #3: 0x000000010894d422 Foundation`NSKeyValueDidChangeWithPerThreadPendingNotifications + 148
frame #4: 0x0000000108879b47 Foundation`-[NSObject(NSKeyValueCoding) setValue:forKey:] + 292
frame #5: 0x0000000106be19aa TestKVO`::-[ViewController viewDidLoad](self=0x00007fe68ac07710, _cmd="viewDidLoad") at ViewController.mm:70
實現(xiàn)原理
核心邏輯
KVO
是通過isa-swizzling
技術(shù)實現(xiàn)的膀懈,這是整個KVO
實現(xiàn)的重點顿锰。在運(yùn)行時根據(jù)原類創(chuàng)建一個中間類,這個中間類是原類的子類,并動態(tài)修改當(dāng)前對象的isa
指向中間類撵儿。并且將class
方法重寫乘客,返回原類的Class
。蘋果重寫class
方法淀歇,就是為了屏蔽中間類的存在易核。
所以,蘋果建議在開發(fā)中不應(yīng)該依賴isa
指針浪默,而是通過class
實例方法來獲取對象類型牡直,來避免被KVO
或者其他runtime
方法影響。
_NSSetObjectValueAndNotify
隨后會修改中間類對應(yīng)的set
方法纳决,并且插入willChangeValueForkey
方法以及didChangeValueForKey
方法碰逸,在兩個方法中間調(diào)用父類的set
方法。這個過程阔加,系統(tǒng)將其封裝到_NSSetObjectValueAndNotify
函數(shù)中饵史。通過查看這個函數(shù)的匯編代碼,可以看到內(nèi)部封裝的willChangeValueForkey
方法和didChangeValueForKey
方法的調(diào)用胜榔。
系統(tǒng)并不是只封裝了_NSSetObjectValueAndNotify
函數(shù)胳喷,而是會根據(jù)屬性類型,調(diào)用不同的函數(shù)夭织。如果是Int
類型就會調(diào)用_NSSetIntValueAndNotify
吭露,這些實現(xiàn)都定義在Foundation
框架中。具體的可以通過hopper
來查看Foundation
框架的實現(xiàn)尊惰。
runtime
會將新生成的NSKVONotifying_KVOTest
的setObject
方法的實現(xiàn)讲竿,替換成_NSSetObjectValueAndNotify
函數(shù),而不是重寫setObject
函數(shù)弄屡。通過下面的測試代碼题禀,可以查看selector
對應(yīng)的IMP
,并且將其實現(xiàn)的地址打印出來膀捷。
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:)]);
// 打印結(jié)果迈嘹,第一次的方法地址為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
對于系統(tǒng)實現(xiàn)KVO
的原理担孔,可以對object_setClass
打斷點江锨,或者對objc_allocateClassPair
方法打斷點也可以吃警,這兩個方法都是創(chuàng)建類必走的方法糕篇。通過這兩個方法的匯編堆棧拌消,向前回溯氓英。隨后铝阐,可以得到翻譯后如下的匯編代碼。
可以看到有一些類名拼接規(guī)則遍蟋,隨后根據(jù)類名創(chuàng)建新類它呀。如果newCls
為空則已經(jīng)創(chuàng)建過纵穿,或者可能為空政恍。如果newCls
不為空,則注冊新創(chuàng)建的類宗弯,并且設(shè)置SDTestKVOClassIndexedIvars
結(jié)構(gòu)體的一些參數(shù)蒙保。
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
的實現(xiàn)方式扁瓢,我們加入下面的測試代碼引几。首先創(chuàng)建一個KVOObject
類敞掘,并在里面加入兩個屬性玖雁,然后重寫description
方法疯潭,并在內(nèi)部打印一些關(guān)鍵參數(shù)竖哩。
需要注意的是相叁,為了驗證KVO
在運(yùn)行時做了什么,我打印了對象的class
方法虑润,以及通過runtime
獲取對象的類和父類拳喻。在添加KVO
監(jiān)聽前后冗澈,都打印一次亚亲,觀察系統(tǒng)做了什么。
@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 @"";
}
創(chuàng)建一個KVOObject
對象惜索,在KVO
前后分別打印對象的關(guān)鍵信息,看KVO
前后有什么變化偿渡。
self.object = [[KVOObject alloc] init];
[self.object description];
[self.object addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object description];
下面是KVO
前后打印的關(guān)鍵信息吉拳。
我們發(fā)現(xiàn)對象被KVO
后留攒,其真正類型變?yōu)榱?code>NSKVONotifying_KVOObject類,已經(jīng)不是之前的類了剪侮。KVO
會在運(yùn)行時動態(tài)創(chuàng)建一個新類杰标,將對象的isa
指向新創(chuàng)建的類腔剂,并且將superClass
指向原來的類KVOObject
掸犬,新創(chuàng)建的類命名規(guī)則是NSKVONotifying_xxx
的格式。KVO
為了使其更像之前的類胜茧,還會將對象的class
實例方法重寫呻顽,使其更像原類廊遍。
添加KVO
之后喉前,由于修改了setName
方法和setAge
方法的IMP
卵迂,所以打印這兩個方法的IMP
见咒,也是一個新的地址下翎,新的實現(xiàn)在NSKVONotifying_KVOObject
中宝当。
這種實現(xiàn)方式對業(yè)務(wù)代碼沒有侵入性郑口,可以在不影響KVOObject
其他對象的前提下犬性,對單個對象進(jìn)行監(jiān)聽并修改其方法實現(xiàn)乒裆,在賦值時觸發(fā)KVO
回調(diào)鹤耍。
在上面的代碼中還發(fā)現(xiàn)了_isKVOA
方法稿黄,這個方法可以當(dāng)做使用了KVO
的一個標(biāo)記,系統(tǒng)可能也是這么用的陵珍。如果我們想判斷當(dāng)前類是否是KVO
動態(tài)生成的類互纯,就可以從方法列表中搜索這個方法留潦。
// 第一次
object address : 0x604000239340
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
// 第二次
object address : 0x604000239340
object setName: IMP 0x10ea8defe object setAge: IMP 0x10ea94106
objectMethodClass : KVOObject, ObjectRuntimeClass : NSKVONotifying_KVOObject, superClass : KVOObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA
object_getClass
為什么上面調(diào)用runtime
的object_getClass
函數(shù)殖卑,就可以獲取到真正的類呢?
調(diào)用object_getClass
函數(shù)后其返回的是一個Class
類型屹堰,Class
是objc_class
定義的一個typedef
別名扯键,通過objc_class
就可以獲取到對象的isa
指針指向的Class
荣刑,也就是對象的類對象。
由此可以知道爱只,object_getClass
函數(shù)內(nèi)部返回的是對象的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
需要是成對的,如果重復(fù)remove
則會導(dǎo)致NSRangeException
類型的Crash
妇拯,如果忘記remove
則會在觀察者釋放后再次接收到KVO
回調(diào)時Crash
宣赔。
蘋果官方推薦的方式是儒将,在init
的時候進(jìn)行addObserver
钩蚊,在dealloc
時removeObserver
砰逻,這樣可以保證add
和remove
是成對出現(xiàn)的踊东,是一種比較理想的使用方式闸翅。
錯誤檢查
如果傳入一個錯誤的keyPath
并不會有錯誤提示坚冀。在調(diào)用KVO
時需要傳入一個keyPath
,由于keyPath
是字符串的形式液南,如果屬性名發(fā)生改變后贺拣,字符串沒有改變?nèi)菀讓?dǎo)致Crash
。對于這個問題啥辨,我們可以利用系統(tǒng)的反射機(jī)制將keyPath
反射出來陨瘩,這樣編譯器可以在@selector()
中進(jìn)行合法性檢查舌劳。
NSString *keyPath = NSStringFromSelector(@selector(isFinished));
不能觸發(fā)回調(diào)
由于KVO
的實現(xiàn)機(jī)制甚淡,如果調(diào)用成員變量進(jìn)行賦值资柔,是不會觸發(fā)KVO
的贿堰。
@interface TestObject : NSObject {
@public
NSObject *object;
}
@end
// 錯誤的調(diào)用方式
self.object = [[TestObject alloc] init];
[self.object addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];
self.object->object = [[NSObject alloc] init];
但是羹与,如果通過KVC
的方式調(diào)用賦值操作纵搁,則會觸發(fā)KVO
的回調(diào)方法捐晶。這是因為KVC
對KVO
有單獨的兼容山上,在KVC
的賦值方法內(nèi)部,手動調(diào)用了willChangeValueForKey:
和didChangeValueForKey:
方法妄帘。
// KVC的方式調(diào)用
self.object = [[TestObject alloc] init];
[self.object addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];
[self.object setValue:[[NSObject alloc] init] forKey:@"object"];
重復(fù)添加
對KVO
進(jìn)行重復(fù)addObserver
并不會導(dǎo)致崩潰,但是會出現(xiàn)重復(fù)執(zhí)行KVO
回調(diào)方法的問題致盟。
[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] test
2018-08-03 11:48:52.975102+0800 KVOTest[5846:412257] test
2018-08-03 11:48:53.547145+0800 KVOTest[5846:412257] test
2018-08-03 11:48:54.087171+0800 KVOTest[5846:412257] test
2018-08-03 11:48:54.649244+0800 KVOTest[5846:412257] test
通過上面的測試代碼,并且在回調(diào)中打印object
所對應(yīng)的Class
來看尤慰,并不會重復(fù)創(chuàng)建子類馏锡,始終都是一個類。雖然重復(fù)addobserver
不會立刻崩潰伟端,但是重復(fù)添加后在第一次調(diào)用removeObserver
時杯道,就會立刻崩潰。從崩潰堆棧來看责蝠,和重復(fù)移除的問題一樣蕉饼,都是系統(tǒng)主動拋出的異常。
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.'
重復(fù)移除
KVO
是不允許對一個keyPath
進(jìn)行重復(fù)移除的达舒,如果重復(fù)移除贯底,則會導(dǎo)致崩潰胚想。例如下面的測試代碼牙躺。
[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"];
執(zhí)行上面的測試代碼后代虾,會造成下面的崩潰信息环形。從KVO
的崩潰堆椢6樱可以看出來,系統(tǒng)為了實現(xiàn)KVO
的addObserver
和removeObserver
,為NSObject
添加了一個名為NSKeyValueObserverRegistration
的Category
解取,KVO
的addObserver
和removeObserver
的實現(xiàn)都在里面蔗包。
在移除KVO
的監(jiān)聽時,系統(tǒng)會判斷當(dāng)前KVO
的keyPath
是否已經(jīng)被移除哨免,如果已經(jīng)被移除身辨,則主動拋出一個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
是一種事件綁定機(jī)制的實現(xiàn)亮隙,在keyPath
對應(yīng)的值發(fā)生改變后會回調(diào)對應(yīng)的方法。這種數(shù)據(jù)綁定機(jī)制,在對象關(guān)系很復(fù)雜的情況下,很容易導(dǎo)致不好排查的bug
摔笤。例如keyPath
對應(yīng)的屬性被調(diào)用的關(guān)系很復(fù)雜,就不太建議對這個屬性進(jìn)行KVO
。
自己實現(xiàn)KVO
除了上面的缺點,KVO
還不支持block
語法,需要單獨重寫父類方法愉粤,這樣加上add
和remove
方法就會導(dǎo)致代碼很分散早歇。所以狈癞,我通過runtime
簡單的實現(xiàn)了一個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) {
// 處理業(yè)務(wù)邏輯
}];
self.object.name = @"lxz";
// 移除通知
[self.object lxz_removeObserver:self originalSelector:@selector(name)];
調(diào)用代碼很簡單屠缭,直接通過lxz_addObserver:originalSelector:callback:
方法就可以添加KVO
的監(jiān)聽奄喂,可以通過callback
的block
接收屬性發(fā)生改變后的回調(diào)俯树。而且方法的keyPath
接收的是一個SEL
類型參數(shù),所以可以通過@selector()
傳入?yún)?shù)時進(jìn)行方法合法性檢查,如果是未實現(xiàn)的方法直接就會報警告楚堤。
通過lxz_removeObserver:originalSelector:
方法傳入觀察者和keyPath
疫蔓,當(dāng)觀察者所有keyPath
都移除后則從KVO
中移除觀察者對象。
如果重復(fù)addObserver
和removeObserver
也沒事身冬,內(nèi)部有判斷邏輯衅胀。EasyKVO
內(nèi)部通過weak
對觀察者做引用,并不會影響觀察者的生命周期酥筝,并且在觀察者釋放后不會導(dǎo)致Crash
拗小。一次add
方法調(diào)用對應(yīng)一個block
,如果觀察者監(jiān)聽多個keyPath
屬性樱哼,不需要在block
回調(diào)中判斷keyPath
哀九。
KVOController
想在項目中安全便捷的使用KVO
的話,推薦Facebook
的一個KVO
開源第三方框架KVOController搅幅。KVOController
本質(zhì)上是對系統(tǒng)KVO
的封裝阅束,具有原生KVO
所有的功能,而且規(guī)避了原生KVO
的很多問題茄唐,兼容block
和action
兩種回調(diào)方式息裸。
源碼分析
從源碼來看還是比較簡單的,主要分為NSObject
的Category
和FBKVOController
兩部分沪编。
在Category
中提供了KVOController
和KVOControllerNonRetaining
兩個屬性呼盆,顧名思義第一個會對observer
產(chǎn)生強(qiáng)引用,第二個則不會蚁廓。其內(nèi)部代碼就是創(chuàng)建FBKVOController
對象的代碼访圃,并將創(chuàng)建出來的對象賦值給Category
的屬性,直接通過這個Category
就可以懶加載創(chuàng)建FBKVOController
對象相嵌。
- (FBKVOController *)KVOControllerNonRetaining
{
id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey);
if (nil == controller) {
controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
self.KVOControllerNonRetaining = controller;
}
return controller;
}
實現(xiàn)原理
在FBKVOController
中分為三部分腿时,_FBKVOInfo
是一個私有類况脆,這個類的功能很簡單,就是以結(jié)構(gòu)化的形式保存FBKVOController
所需的各個對象批糟,類似于模型類的功能格了。
還有一個私有類_FBKVOSharedController
,這是FBKVOController
框架實現(xiàn)的關(guān)鍵徽鼎。從命名上可以看出其是一個單例盛末,所有通過FBKVOController
實現(xiàn)的KVO
,觀察者都是它否淤。每次通過FBKVOController
添加一個KVO
時悄但,_FBKVOSharedController
都會將自己設(shè)為觀察者,并在其內(nèi)部實現(xiàn)observeValueForKeyPath:ofObject:change:context:
方法叹括,將接收到的消息通過block
或action
進(jìn)行轉(zhuǎn)發(fā)算墨。
其功能很簡單,通過observe:info:
方法添加KVO
監(jiān)聽汁雷,并用一個NSHashTable
保存_FBKVOInfo
信息净嘀。通過unobserve:info:
方法移除監(jiān)聽,并從NSHashTable
中將對應(yīng)的_FBKVOInfo
移除侠讯。這兩個方法內(nèi)部都會調(diào)用系統(tǒng)的KVO
方法挖藏。
在外界使用時需要用FBKVOController
類,其內(nèi)部實現(xiàn)了初始化以及添加和移除監(jiān)聽的操作厢漩。在調(diào)用添加監(jiān)聽方法后膜眠,其內(nèi)部會創(chuàng)建一個_FBKVOInfo
對象,并通過一個NSMapTable
對象進(jìn)行持有溜嗜,然后會調(diào)用_FBKVOSharedController
來進(jìn)行注冊監(jiān)聽宵膨。
使用FBKVOController
的話,不需要手動調(diào)用removeObserver
方法炸宵,在被監(jiān)聽對象消失的時候辟躏,會在dealloc
中調(diào)用remove
方法。如果因為業(yè)務(wù)需求土全,可以手動調(diào)用remove
方法捎琐,重復(fù)調(diào)用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
的實現(xiàn)很簡單裹匙,所以這里就很簡單的講講瑞凑,具體實現(xiàn)可以去Github下載源碼仔細(xì)分析一下。