介紹
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: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
觸發的向上回撥,可以看到正常通過修改屬性的方式觸發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: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
實現原理
核心邏輯
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 : 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
為什麼上面呼叫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] 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
通過上面的測試程式碼,並且在回撥中列印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下載原始碼仔細分析一下。