KVO原理分析介紹
作者丨搜狐視頻 劉壯
來(lái)源丨搜狐技術(shù)產(chǎn)品(ID:sohu-tech)
本文字?jǐn)?shù):5154字
預(yù)計(jì)閱讀時(shí)間:15分鐘
KVO原理分析
介紹
KVO全稱KeyValueObserving,是蘋(píng)果提供的一套事件通知機(jī)制。允許對(duì)象監(jiān)聽(tīng)另一個(gè)對(duì)象特定屬性的改變,并在改變時(shí)接收到事件。由于KVO的實(shí)現(xiàn)機(jī)制,所以對(duì)屬性才會(huì)發(fā)生作用,一般繼承自NSObject的對(duì)象都默認(rèn)支持KVO。
KVO和NSNotificationCenter都是iOS中觀察者模式的一種實(shí)現(xiàn)。區(qū)別在于,相對(duì)于被觀察者和觀察者之間的關(guān)系,KVO是一對(duì)一的,而不一對(duì)多的。KVO對(duì)被監(jiān)聽(tīng)對(duì)象無(wú)侵入性,不需要手動(dòng)修改其內(nèi)部代碼即可實(shí)現(xiàn)監(jiān)聽(tīng)。
KVO可以監(jiān)聽(tīng)單個(gè)屬性的變化,也可以監(jiān)聽(tīng)集合對(duì)象的變化。通過(guò)KVC的mutableArrayValueForKey:等方法獲得代理對(duì)象,當(dāng)代理對(duì)象的內(nèi)部對(duì)象發(fā)生改變時(shí),會(huì)回調(diào)KVO監(jiān)聽(tīng)的方法。集合對(duì)象包含NSArray和NSSet。
使用
使用KVO分為三個(gè)步驟
通過(guò) addObserver:forKeyPath:options:context:方法注冊(cè)觀察者,觀察者可以接收keyPath屬性的變化事件回調(diào)。在觀察者中實(shí)現(xiàn) observeValueForKeyPath:ofObject:change:context:方法,當(dāng)keyPath屬性發(fā)生改變后,KVO會(huì)回調(diào)這個(gè)方法來(lái)通知觀察者。當(dāng)觀察者不需要監(jiān)聽(tīng)時(shí),可以調(diào)用 removeObserver:forKeyPath:方法將KVO移除。需要注意的是,調(diào)用removeObserver需要在觀察者消失之前,否則會(huì)導(dǎo)致Crash。
注冊(cè)
在注冊(cè)觀察者時(shí),可以傳入options參數(shù),參數(shù)是一個(gè)枚舉類型。如果傳入NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld表示接收新值和舊值,默認(rèn)為只接收新值。如果想在注冊(cè)觀察者后,立即接收一次回調(diào),則可以加入NSKeyValueObservingOptionInitial枚舉。
還可以通過(guò)方法context傳入任意類型的對(duì)象,在接收消息回調(diào)的代碼中可以接收到這個(gè)對(duì)象,是KVO中的一種傳值方式。
在調(diào)用addObserver方法后,KVO并不會(huì)對(duì)觀察者進(jìn)行強(qiáng)引用。所以需要注意觀察者的生命周期,否則會(huì)導(dǎo)致觀察者被釋放帶來(lái)的Crash。
監(jiān)聽(tīng)
觀察者需要實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:方法,當(dāng)KVO事件到來(lái)時(shí)會(huì)調(diào)用這個(gè)方法,如果沒(méi)有實(shí)現(xiàn)會(huì)導(dǎo)致Crash。change字典中存放KVO屬性相關(guān)的值,根據(jù)options時(shí)傳入的枚舉來(lái)返回。枚舉會(huì)對(duì)應(yīng)相應(yīng)key來(lái)從字典中取出值,例如有NSKeyValueChangeOldKey字段,存儲(chǔ)改變之前的舊值。
change中還有NSKeyValueChangeKindKey字段,和NSKeyValueChangeOldKey是平級(jí)的關(guān)系,來(lái)提供本次更改的信息,對(duì)應(yīng)NSKeyValueChange枚舉類型的value。例如被觀察屬性發(fā)生改變時(shí),字段為NSKeyValueChangeSetting。
如果被觀察對(duì)象是集合對(duì)象,在NSKeyValueChangeKindKey字段中會(huì)包含NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement的信息,表示集合對(duì)象的操作方式。
其他觸發(fā)方法
調(diào)用KVO屬性對(duì)象時(shí),不僅可以通過(guò)點(diǎn)語(yǔ)法和set語(yǔ)法進(jìn)行調(diào)用,KVO兼容很多種調(diào)用方式。
// 直接調(diào)用set方法,或者通過(guò)屬性的點(diǎn)語(yǔ)法間接調(diào)用
[account setName:@"Savings"];
// 使用KVC的setValue:forKey:方法
[account setValue:@"Savings" forKey:@"name"];
// 使用KVC的setValue:forKeyPath:方法
[document setValue:@"Savings" forKeyPath:@"account.name"];
// 通過(guò)mutableArrayValueForKey:方法獲取到代理對(duì)象,并使用代理對(duì)象進(jìn)行操作
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
實(shí)際應(yīng)用
KVO主要用來(lái)做鍵值觀察操作,想要一個(gè)值發(fā)生改變后通知另一個(gè)對(duì)象,則用KVO實(shí)現(xiàn)最為合適。斯坦福大學(xué)的iOS教程中有一個(gè)很經(jīng)典的案例,通過(guò)KVO在Model和Controller之間進(jìn)行通信。

觸發(fā)
主動(dòng)觸發(fā)
KVO在屬性發(fā)生改變時(shí)的調(diào)用是自動(dòng)的,如果想要手動(dòng)控制這個(gè)調(diào)用時(shí)機(jī),或想自己實(shí)現(xiàn)KVO屬性的調(diào)用,則可以通過(guò)KVO提供的方法進(jìn)行調(diào)用。
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
可以看到調(diào)用KVO主要依靠?jī)蓚€(gè)方法,在屬性發(fā)生改變之前調(diào)用willChangeValueForKey:方法,在發(fā)生改變之后調(diào)用didChangeValueForKey:方法。但是,如果不調(diào)用willChangeValueForKey,直接調(diào)用didChangeValueForKey是不生效的,二者有先后順序并且需要成對(duì)出現(xiàn)。
禁用KVO
如果想禁止某個(gè)屬性的KVO,例如關(guān)鍵信息不想被三方SDK通過(guò)KVO的方式獲取,可以通過(guò)automaticallyNotifiesObserversForKey方法返回NO來(lái)禁止其他地方對(duì)這個(gè)屬性進(jìn)行KVO。方法返回YES則表示可以調(diào)用,如果返回NO則表示不可以調(diào)用。此方法是一個(gè)類方法,可以在方法內(nèi)部判斷keyPath,來(lái)選擇這個(gè)屬性是否允許被KVO。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
KVC觸發(fā)
KVC對(duì)KVO有特殊兼容,當(dāng)通過(guò)KVC調(diào)用非屬性的實(shí)例變量時(shí),KVC內(nèi)部也會(huì)觸發(fā)KVO的回調(diào),并通過(guò)NSKeyValueDidChange和NSKeyValueWillChange向上回調(diào)。
下面忽略main函數(shù)向上的系統(tǒng)函數(shù),只保留關(guān)鍵堆棧。這是通過(guò)調(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
這是通過(guò)KVC觸發(fā)的向上回調(diào),可以看到正常通過(guò)修改屬性的方式觸發(fā)KVO,和通過(guò)KVC觸發(fā)的KVO還是有區(qū)別的。通過(guò)KVC的方式觸發(fā)KVO,甚至都沒(méi)有_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實(shí)現(xiàn)原理
核心邏輯
KVO是通過(guò)isa-swizzling技術(shù)實(shí)現(xiàn)的,這是整個(gè)KVO實(shí)現(xiàn)的重點(diǎn)。在運(yùn)行時(shí)根據(jù)原類創(chuàng)建一個(gè)中間類,這個(gè)中間類是原類的子類,并動(dòng)態(tài)修改當(dāng)前對(duì)象的isa指向中間類。并且將class方法重寫(xiě),返回原類的Class。蘋(píng)果重寫(xiě)class方法,就是為了屏蔽中間類的存在。
所以,蘋(píng)果建議在開(kāi)發(fā)中不應(yīng)該依賴isa指針,而是通過(guò)class實(shí)例方法來(lái)獲取對(duì)象類型,來(lái)避免被KVO或者其他runtime方法影響。
_NSSetObjectValueAndNotify
隨后會(huì)修改中間類對(duì)應(yīng)的set方法,并且插入willChangeValueForkey方法以及didChangeValueForKey方法,在兩個(gè)方法中間調(diào)用父類的set方法。這個(gè)過(guò)程,系統(tǒng)將其封裝到_NSSetObjectValueAndNotify函數(shù)中。通過(guò)查看這個(gè)函數(shù)的匯編代碼,可以看到內(nèi)部封裝的willChangeValueForkey方法和didChangeValueForKey方法的調(diào)用。
系統(tǒng)并不是只封裝了_NSSetObjectValueAndNotify函數(shù),而是會(huì)根據(jù)屬性類型,調(diào)用不同的函數(shù)。如果是Int類型就會(huì)調(diào)用_NSSetIntValueAndNotify,這些實(shí)現(xiàn)都定義在Foundation框架中。具體的可以通過(guò)hopper來(lái)查看Foundation框架的實(shí)現(xiàn)。
runtime會(huì)將新生成的NSKVONotifying_KVOTest的setObject方法的實(shí)現(xiàn),替換成_NSSetObjectValueAndNotify函數(shù),而不是重寫(xiě)setObject函數(shù)。通過(guò)下面的測(cè)試代碼,可以查看selector對(duì)應(yīng)的IMP,并且將其實(shí)現(xiàn)的地址打印出來(lái)。
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
對(duì)于系統(tǒng)實(shí)現(xiàn)KVO的原理,可以對(duì)object_setClass打斷點(diǎn),或者對(duì)objc_allocateClassPair方法打斷點(diǎn)也可以,這兩個(gè)方法都是創(chuàng)建類必走的方法。通過(guò)這兩個(gè)方法的匯編堆棧,向前回溯。隨后,可以得到翻譯后如下的匯編代碼。
可以看到有一些類名拼接規(guī)則,隨后根據(jù)類名創(chuàng)建新類。如果newCls為空則已經(jīng)創(chuàng)建過(guò),或者可能為空。如果newCls不為空,則注冊(cè)新創(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;
}
驗(yàn)證
為了驗(yàn)證KVO的實(shí)現(xiàn)方式,我們加入下面的測(cè)試代碼。首先創(chuàng)建一個(gè)KVOObject類,并在里面加入兩個(gè)屬性,然后重寫(xiě)description方法,并在內(nèi)部打印一些關(guān)鍵參數(shù)。
需要注意的是,為了驗(yàn)證KVO在運(yùn)行時(shí)做了什么,我打印了對(duì)象的class方法,以及通過(guò)runtime獲取對(duì)象的類和父類。在添加KVO監(jiān)聽(tīng)前后,都打印一次,觀察系統(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)建一個(gè)KVOObject對(duì)象,在KVO前后分別打印對(duì)象的關(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)對(duì)象被KVO后,其真正類型變?yōu)榱?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(244, 138, 0);">NSKVONotifying_KVOObject類,已經(jīng)不是之前的類了。KVO會(huì)在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建一個(gè)新類,將對(duì)象的isa指向新創(chuàng)建的類,并且將superClass指向原來(lái)的類KVOObject,新創(chuàng)建的類命名規(guī)則是NSKVONotifying_xxx的格式。KVO為了使其更像之前的類,還會(huì)將對(duì)象的class實(shí)例方法重寫(xiě),使其更像原類。
添加KVO之后,由于修改了setName方法和setAge方法的IMP,所以打印這兩個(gè)方法的IMP,也是一個(gè)新的地址,新的實(shí)現(xiàn)在NSKVONotifying_KVOObject中。
這種實(shí)現(xiàn)方式對(duì)業(yè)務(wù)代碼沒(méi)有侵入性,可以在不影響KVOObject其他對(duì)象的前提下,對(duì)單個(gè)對(duì)象進(jìn)行監(jiān)聽(tīng)并修改其方法實(shí)現(xiàn),在賦值時(shí)觸發(fā)KVO回調(diào)。
在上面的代碼中還發(fā)現(xiàn)了_isKVOA方法,這個(gè)方法可以當(dāng)做使用了KVO的一個(gè)標(biāo)記,系統(tǒng)可能也是這么用的。如果我們想判斷當(dāng)前類是否是KVO動(dòng)態(tài)生成的類,就可以從方法列表中搜索這個(gè)方法。
// 第一次
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ù)后其返回的是一個(gè)Class類型,Class是objc_class定義的一個(gè)typedef別名,通過(guò)objc_class就可以獲取到對(duì)象的isa指針指向的Class,也就是對(duì)象的類對(duì)象。
由此可以知道,object_getClass函數(shù)內(nèi)部返回的是對(duì)象的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
}
注意點(diǎn)
Crash
KVO的addObserver和removeObserver需要是成對(duì)的,如果重復(fù)remove則會(huì)導(dǎo)致NSRangeException類型的Crash,如果忘記remove則會(huì)在觀察者釋放后再次接收到KVO回調(diào)時(shí)Crash。
蘋(píng)果官方推薦的方式是,在init的時(shí)候進(jìn)行addObserver,在dealloc時(shí)removeObserver,這樣可以保證add和remove是成對(duì)出現(xiàn)的,是一種比較理想的使用方式。
錯(cuò)誤檢查
如果傳入一個(gè)錯(cuò)誤的keyPath并不會(huì)有錯(cuò)誤提示。在調(diào)用KVO時(shí)需要傳入一個(gè)keyPath,由于keyPath是字符串的形式,如果屬性名發(fā)生改變后,字符串沒(méi)有改變?nèi)菀讓?dǎo)致Crash。對(duì)于這個(gè)問(wèn)題,我們可以利用系統(tǒng)的反射機(jī)制將keyPath反射出來(lái),這樣編譯器可以在@selector()中進(jìn)行合法性檢查。
NSString *keyPath = NSStringFromSelector(@selector(isFinished));
不能觸發(fā)回調(diào)
由于KVO的實(shí)現(xiàn)機(jī)制,如果調(diào)用成員變量進(jìn)行賦值,是不會(huì)觸發(fā)KVO的。
@interface TestObject : NSObject {
@public
NSObject *object;
}
@end
// 錯(cuò)誤的調(diào)用方式
self.object = [[TestObject alloc] init];
[self.object addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];
self.object->object = [[NSObject alloc] init];
但是,如果通過(guò)KVC的方式調(diào)用賦值操作,則會(huì)觸發(fā)KVO的回調(diào)方法。這是因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(244, 138, 0);">KVC對(duì)KVO有單獨(dú)的兼容,在KVC的賦值方法內(nèi)部,手動(dòng)調(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ù)添加
對(duì)KVO進(jìn)行重復(fù)addObserver并不會(huì)導(dǎo)致崩潰,但是會(huì)出現(xiàn)重復(fù)執(zhí)行KVO回調(diào)方法的問(wèn)題。
[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
通過(guò)上面的測(cè)試代碼,并且在回調(diào)中打印object所對(duì)應(yīng)的Class來(lái)看,并不會(huì)重復(fù)創(chuàng)建子類,始終都是一個(gè)類。雖然重復(fù)addobserver不會(huì)立刻崩潰,但是重復(fù)添加后在第一次調(diào)用removeObserver時(shí),就會(huì)立刻崩潰。從崩潰堆棧來(lái)看,和重復(fù)移除的問(wèn)題一樣,都是系統(tǒng)主動(dò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是不允許對(duì)一個(gè)keyPath進(jìn)行重復(fù)移除的,如果重復(fù)移除,則會(huì)導(dǎo)致崩潰。例如下面的測(cè)試代碼。
[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í)行上面的測(cè)試代碼后,會(huì)造成下面的崩潰信息。從KVO的崩潰堆??梢钥闯鰜?lái),系統(tǒng)為了實(shí)現(xiàn)KVO的addObserver和removeObserver,為NSObject添加了一個(gè)名為NSKeyValueObserverRegistration的Category,KVO的addObserver和removeObserver的實(shí)現(xiàn)都在里面。
在移除KVO的監(jiān)聽(tīng)時(shí),系統(tǒng)會(huì)判斷當(dāng)前KVO的keyPath是否已經(jīng)被移除,如果已經(jīng)被移除,則主動(dòng)拋出一個(gè)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ī)制的實(shí)現(xiàn),在keyPath對(duì)應(yīng)的值發(fā)生改變后會(huì)回調(diào)對(duì)應(yīng)的方法。這種數(shù)據(jù)綁定機(jī)制,在對(duì)象關(guān)系很復(fù)雜的情況下,很容易導(dǎo)致不好排查的bug。例如keyPath對(duì)應(yīng)的屬性被調(diào)用的關(guān)系很復(fù)雜,就不太建議對(duì)這個(gè)屬性進(jìn)行KVO。
自己實(shí)現(xiàn)KVO
除了上面的缺點(diǎn),KVO還不支持block語(yǔ)法,需要單獨(dú)重寫(xiě)父類方法,這樣加上add和remove方法就會(huì)導(dǎo)致代碼很分散。所以,我通過(guò)runtime簡(jiǎn)單的實(shí)現(xiàn)了一個(gè)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)用代碼很簡(jiǎn)單,直接通過(guò)lxz_addObserver:originalSelector:callback:方法就可以添加KVO的監(jiān)聽(tīng),可以通過(guò)callback的block接收屬性發(fā)生改變后的回調(diào)。而且方法的keyPath接收的是一個(gè)SEL類型參數(shù),所以可以通過(guò)@selector()傳入?yún)?shù)時(shí)進(jìn)行方法合法性檢查,如果是未實(shí)現(xiàn)的方法直接就會(huì)報(bào)警告。
通過(guò)lxz_removeObserver:originalSelector:方法傳入觀察者和keyPath,當(dāng)觀察者所有keyPath都移除后則從KVO中移除觀察者對(duì)象。
如果重復(fù)addObserver和removeObserver也沒(méi)事,內(nèi)部有判斷邏輯。EasyKVO內(nèi)部通過(guò)weak對(duì)觀察者做引用,并不會(huì)影響觀察者的生命周期,并且在觀察者釋放后不會(huì)導(dǎo)致Crash。一次add方法調(diào)用對(duì)應(yīng)一個(gè)block,如果觀察者監(jiān)聽(tīng)多個(gè)keyPath屬性,不需要在block回調(diào)中判斷keyPath。
KVOController
想在項(xiàng)目中安全便捷的使用KVO的話,推薦Facebook的一個(gè)KVO開(kāi)源第三方框架KVOController。KVOController本質(zhì)上是對(duì)系統(tǒng)KVO的封裝,具有原生KVO所有的功能,而且規(guī)避了原生KVO的很多問(wèn)題,兼容block和action兩種回調(diào)方式。
源碼分析
從源碼來(lái)看還是比較簡(jiǎn)單的,主要分為NSObject的Category和FBKVOController兩部分。

在Category中提供了KVOController和KVOControllerNonRetaining兩個(gè)屬性,顧名思義第一個(gè)會(huì)對(duì)observer產(chǎn)生強(qiáng)引用,第二個(gè)則不會(huì)。其內(nèi)部代碼就是創(chuàng)建FBKVOController對(duì)象的代碼,并將創(chuàng)建出來(lái)的對(duì)象賦值給Category的屬性,直接通過(guò)這個(gè)Category就可以懶加載創(chuàng)建FBKVOController對(duì)象。
- (FBKVOController *)KVOControllerNonRetaining
{
id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey);
if (nil == controller) {
controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
self.KVOControllerNonRetaining = controller;
}
return controller;
}
實(shí)現(xiàn)原理
在FBKVOController中分為三部分,_FBKVOInfo是一個(gè)私有類,這個(gè)類的功能很簡(jiǎn)單,就是以結(jié)構(gòu)化的形式保存FBKVOController所需的各個(gè)對(duì)象,類似于模型類的功能。
還有一個(gè)私有類_FBKVOSharedController,這是FBKVOController框架實(shí)現(xiàn)的關(guān)鍵。從命名上可以看出其是一個(gè)單例,所有通過(guò)FBKVOController實(shí)現(xiàn)的KVO,觀察者都是它。每次通過(guò)FBKVOController添加一個(gè)KVO時(shí),_FBKVOSharedController都會(huì)將自己設(shè)為觀察者,并在其內(nèi)部實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:方法,將接收到的消息通過(guò)block或action進(jìn)行轉(zhuǎn)發(fā)。
其功能很簡(jiǎn)單,通過(guò)observe:info:方法添加KVO監(jiān)聽(tīng),并用一個(gè)NSHashTable保存_FBKVOInfo信息。通過(guò)unobserve:info:方法移除監(jiān)聽(tīng),并從NSHashTable中將對(duì)應(yīng)的_FBKVOInfo移除。這兩個(gè)方法內(nèi)部都會(huì)調(diào)用系統(tǒng)的KVO方法。
在外界使用時(shí)需要用FBKVOController類,其內(nèi)部實(shí)現(xiàn)了初始化以及添加和移除監(jiān)聽(tīng)的操作。在調(diào)用添加監(jiān)聽(tīng)方法后,其內(nèi)部會(huì)創(chuàng)建一個(gè)_FBKVOInfo對(duì)象,并通過(guò)一個(gè)NSMapTable對(duì)象進(jìn)行持有,然后會(huì)調(diào)用_FBKVOSharedController來(lái)進(jìn)行注冊(cè)監(jiān)聽(tīng)。
使用FBKVOController的話,不需要手動(dòng)調(diào)用removeObserver方法,在被監(jiān)聽(tīng)對(duì)象消失的時(shí)候,會(huì)在dealloc中調(diào)用remove方法。如果因?yàn)闃I(yè)務(wù)需求,可以手動(dòng)調(diào)用remove方法,重復(fù)調(diào)用remove方法不會(huì)有問(wèn)題。
- (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];
}
因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(244, 138, 0);">FBKVOController的實(shí)現(xiàn)很簡(jiǎn)單,所以這里就很簡(jiǎn)單的講講,具體實(shí)現(xiàn)可以去Github下載源碼仔細(xì)分析一下。
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來(lái),可以說(shuō)是程序員面試必備!所有資料都整理到網(wǎng)盤(pán)了,歡迎下載!

面試題】即可獲取