KVO原理分析
介紹
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下載源碼仔細分析一下。
- 天翼雲全場景業務無縫替換至國產原生操作系統CTyunOS!
- 以羊了個羊為例,淺談小程序抓包與響應報文修改
- 這幾種常見的 JVM 調優場景,你知道嗎?
- 如此狂妄,自稱高性能隊列的Disruptor有啥來頭?
- 為什麼要學習GoF設計模式?
- 827. 最大人工島 : 簡單「並查集 枚舉」運用題
- 手把手教你如何使用 Timestream 實現物聯網時序數據存儲和分析
- 850. 矩形面積 II : 掃描線模板題
- Java 併發編程解析 | 基於JDK源碼解析Java領域中的併發鎖,我們可以從中學習到什麼內容?
- 【手把手】光説不練假把式,這篇全鏈路壓測實踐探索
- 大廠鍾愛的全鏈路壓測有什麼意義?四種壓測方案詳細對比分析
- 寫個續集,填坑來了!關於“Thread.sleep(0)這一行‘看似無用’的代碼”裏面留下的坑。
- 857. 僱傭 K 名工人的最低成本 : 枚舉 優先隊列(堆)運用題
- Vue3 實現一個自定義toast(小彈窗)
- 669. 修剪二叉搜索樹 : 常規樹的遍歷與二叉樹性質
- 讀完 RocketMQ 源碼,我學會了如何優雅的創建線程
- 性能調優——小小的log大大的坑
- 1582. 二進制矩陣中的特殊位置 : 簡單模擬題
- elementui源碼學習之仿寫一個el-switch
- 646. 最長數對鏈 : 常規貪心 DP 運用題