面試題目
iOS
用什么方式實現(xiàn)對一個對象的KVO
退敦?(KVO
的本質(zhì)是什么伍俘?)- 如何手動觸發(fā)
KVO
邪锌?
上面兩道面試題目,都是在考察程序員對KVO
的理解癌瘾。KVO
對于一個iOS
程序員來講并不陌生觅丰,在實際開發(fā)中我們或多或少都會使用到,在某些場景中妨退,會讓我們的開發(fā)變得更加簡單妇萄。那么你了解KVO
的本質(zhì)嗎蜕企?它是如何實現(xiàn)的呢?
KVO
簡介
在這里幫大家回憶一下KVO
冠句,對于熟悉的同學向下翻閱轻掩。
概述
NSKeyValueObserving
或者 KVO
,是一個非正式協(xié)議懦底,它定義了對象之間觀察和通知狀態(tài)改變的通用機制的放典。KVO
的中心思想其實是相當引人注意的。任意一個對象都可以訂閱以便被通知到其他對象狀態(tài)的改變基茵。這個過程大部分是內(nèi)建的奋构,自動的,透明的拱层。
KVO
字面翻譯就是鍵值對監(jiān)聽
弥臼,允許對象監(jiān)聽另一個對象特定屬性的改變,并在改變時接收到事件根灯。一般繼承自NSObject
的對象都默認支持KVO
径缅。
KVO
可以監(jiān)聽單個屬性的變化,也可以監(jiān)聽集合對象(NSArray
和NSSet
)的變化烙肺。
基礎(chǔ)使用
- 添加觀察者:通過
addObserver:forKeyPath:options:context:
方法注冊觀察者纳猪,觀察者可以接收keyPath
屬性的變化事件。 - 回調(diào):在觀察者中實現(xiàn)
observeValueForKeyPath:ofObject:change:context:
方法桃笙,當keyPath
屬性發(fā)生改變后氏堤,KVO
會回調(diào)這個方法來通知觀察者。 - 當觀察者不需要監(jiān)聽時搏明,必須調(diào)用
removeObserver:forKeyPath:
方法將KVO
移除鼠锈。
注冊觀察者
/*
observer:注冊KVO通知的對象。觀察者必須實現(xiàn) key-value observing 方法
observeValueForKeyPath:ofObject:change:context:星著。
keyPath:觀察者的屬性的 keypath购笆,相對于接受者,值不能是 nil虚循。
options: NSKeyValueObservingOptions 的組合同欠,它指定了觀察通知中包含了什
么,可以查看 "NSKeyValueObservingOptions"横缔。
context:在 observeValueForKeyPath:ofObject:change:context: 傳給observer
參數(shù)的隨機數(shù)據(jù)
*/
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
options
代表NSKeyValueObservingOptions
的位掩碼铺遂。
NSKeyValueObservingOptionNew = 0x01, 新值
NSKeyValueObservingOptionOld = 0x02, 舊值
NSKeyValueObservingOptionInitial = 0x04, 在注冊觀察者后,立即接收一次回調(diào)
NSKeyValueObservingOptionPrior = 0x08 會在變化前后收到兩次回調(diào)
context
它可以被用作區(qū)分那些綁定同一個keypath
的不同對象的觀察者剪廉。如何設置一個好的context
?
static void *XXContext = &XXContext;
一個靜態(tài)變量存著它自己的指針娃循。在CocoaAsynSocket
中也有類似的使用。
當然context
還可以傳入任意類型的對象斗蒋,在接收消息回調(diào)的代碼中可以接收到這個對象捌斧,是KVO
中的一種傳值方式。
回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
這些參數(shù)跟我們指定的 –addObserver:forKeyPath:options:context:
是一樣的泉沾,而change
取決于哪個NSKeyValueObservingOptions
選項被使用捞蚂。
更好的KeyPath
傳字符串做為keypath
比直接使用屬性更糟糕,因為任何錯字或者拼寫錯誤都不會被編譯器察覺跷究,最終導致不能正常工作姓迅。 一個聰明的解決方案是使用 NSStringFromSelector
和一個@selector
字面值。
NSStringFromSelector(@selector(isFinished))
因為@selector
檢查目標中的所有可用selector
俊马,這并不能阻止所有的錯誤丁存,但它可用捕獲大部分-包括捕獲Xcode
自動重構(gòu)帶來的改變。
那么在回調(diào)中可能是這樣子寫的:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([object isKindOfClass:[NSOperation class]]) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(isFinished))]) {
}
} else if (...) {
// ...
}
}
取消注冊
當一個觀察者完成了監(jiān)聽一個對象的改變柴我,需要調(diào)用removeObserver:forKeyPath:context:
解寝。它經(jīng)常在observeValueForKeyPath:ofObject:change:context:
,或者dealloc
中被調(diào)用艘儒。
注意
- 在調(diào)用
addObserver
方法后聋伦,KVO
并不會對觀察者進行強引用,所以需要注意觀察者的生命周期界睁。 -
KVO
的addObserver
和removeObserver
必須成對出現(xiàn)觉增,如果重復removeObserver
則會導致Crash
,如果在觀察者釋放前翻斟,忘記removeObserver
逾礁,則會在再次接收到KVO
回調(diào)時Crash
。 - 觀察者必須實現(xiàn)
key-value observing
方法
observeValueForKeyPath:ofObject:change:context:
,否則會Crash
访惜。 -
KVO
也有對應集合的實現(xiàn)敞斋,包括我們常用的NSArray
,可以去KVO
的頭文件中查看疾牲,有對應的鍵值參數(shù)等植捎。
蘋果官方推薦的方式是,在init
的時候進行addObserver
阳柔,在dealloc
時removeObserver
焰枢,這樣可以保證add
和remove
是成對出現(xiàn)的,是一種比較理想的使用方式舌剂。
KVO
本質(zhì)
我們可以翻看蘋果文檔:
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
KVO
是通過基于runtime
技術(shù)實現(xiàn)的济锄。當某個對象被觀察時,系統(tǒng)的runtime運行時會動態(tài)的實現(xiàn)一個基于該類的一個中間類(派生類)霍转,在中間類中實現(xiàn)被觀察基類屬性的setter方法荐绝,中間類在重寫的setter方法中實現(xiàn)真正的通知機制。
同時派生類還重寫了- (Class)class;
方法以“欺騙”外部調(diào)用者它就是起初的那個類避消。然后系統(tǒng)將被觀察對象的isa
指針指向這個新誕生的派生類低滩,因此這個對象就成為該派生類的對象了召夹,因而在該對象上對被觀察屬性setter
的調(diào)用就會調(diào)用重寫的 setter
,從而激活鍵值通知機制恕沫。此外监憎,派生類還重寫了dealloc
方法來釋放資源。
通過代碼驗證一波:
/* 被觀察對象模型 */
@interface KVOObject : NSObject
@property (nonatomic, copy) NSString *objName;
@end
/* 在控制器的viewDidLoad中 */
- (void)viewDidLoad {
[super viewDidLoad];
self.object_1 = [[KVOObject alloc] init];
self.object_1.objName = @"object_1---前";
[self isaPointerOfObject:self.object_1];
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld;
[self.object_1 addObserver:self forKeyPath:@"objName" options:options
context:kObserverContext];
self.object_1.objName = @"object_1---后";
[self isaPointerOfObject:self.object_1];
}
/* 打印被觀察對象所屬類... */
- (void)isaPointerOfObject:(KVOObject *)obj{
Class objectMethodClass = [obj class];
Class objectRuntimeClass = object_getClass(obj);
Class superClass = class_getSuperclass(objectRuntimeClass);
NSLog(@"%@-objectMethodClass:%@",obj.objName,objectMethodClass);
NSLog(@"%@-objectRuntimeClass:%@",obj.objName,objectRuntimeClass);
NSLog(@"%@-superClass:%@",obj.objName,superClass);
}
通過控制臺打印的內(nèi)容對比婶溯,對象被KVO
后鲸阔,其真正類型變?yōu)榱?code>NSKVONotifying_KVOObject類,已經(jīng)不是之前的類,其實是將被觀察對象的isa從指向的KVOObject的class迄委,改為系統(tǒng)“偷偷”幫我們創(chuàng)建的NSKVONotifying_KVOObject
類的class褐筛。新類是原類的子類,命名規(guī)則是NSKVONotifying_xxx的格式叙身。
同時我們發(fā)現(xiàn)- (Class)class;
在注冊觀察者前后渔扎,結(jié)果卻是一致的。這其實是蘋果對我們的一種“欺騙”曲梗,在中間類中重寫了- (Class)class
方法赞警。
/* 打印查看方法*/
- (void)methodListOfObject:(KVOObject *)obj
{
Class objectRuntimeClass = object_getClass(obj);
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",obj.objName,methodName);
}
free(methodList);
}
在新創(chuàng)建的中間類中,重寫了setter:
,dealloc
,class
方法虏两,添加新的方法_isKVOA
愧旦。dealloc
推測在添加觀察者時,增加了一些依賴定罢,需要在對象釋放時銷毀笤虫。_isKVOA
方法,這個方法可以當做使用了KVO
的一個標記祖凫,系統(tǒng)可能也是這么用的琼蚯。如果我們想判斷當前類是否是KVO
動態(tài)生成的類,就可以從方法列表中搜索這個方法惠况。
那么系統(tǒng)重寫了setter:
后是如何實現(xiàn)通知的呢遭庶?由于蘋果API這部分沒有開源,但是通過函數(shù)調(diào)用棧稠屠,我們推測一二峦睡。
/* 在KVOObject的.m文件中...*/
@implementation KVOObject
// 是否允許通知..
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
NSLog(@"automaticallyNotifiesObserversForKey:");
return YES;
}
// 重寫setter
- (void)setObjName:(NSString *)objName
{
NSLog(@"setter方法,屬性發(fā)生修改 - 前");
_objName = objName;
NSLog(@"setter方法,屬性發(fā)生修改 - 后");
}
// 重寫willChangeValueForKey
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
// 重寫didChangeValueForKey
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - 前");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - 后");
}
@end
控制臺打印結(jié)果:
函數(shù)調(diào)用棧:
控制臺打印結(jié)果和函數(shù)調(diào)用棧推測:重寫的setter:
內(nèi)部調(diào)用了_NSSetObjectValueAndNotify
的方法(這是一個系列的方法,還有_NSSetIntValueAndNotify
权埠、_NSSetDoubleValueAndNotify
等)榨了。
在這個明顯C
風格命名的方法內(nèi)部,
- 先調(diào)用了
willChangeValueForKey:
- 然后調(diào)用父類的
-setter:
方法攘蔽,對值進行修改 - 修改完后再調(diào)用
didChangeValueForKey:
方法 - 在
didChangeValueForKey:
的內(nèi)部最終調(diào)用了NSKeyValueNotifyObserver
方法龙屉,通知屬性的觀察者,觀察者收到了值修改的信息 -
didChangeValueForKey:
調(diào)用完畢
至此满俗,在能力范圍內(nèi)的對KVO
內(nèi)部的分析已經(jīng)完畢转捕。相信看到這里的同學對 iOS
用什么方式實現(xiàn)對一個對象的KVO
作岖?(KVO
的本質(zhì)是什么?)已經(jīng)可以回答出來了瓜富。
那如何手動觸發(fā)KVO
鳍咱?怎么弄...
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
控制是否自動發(fā)送通知降盹,如果返回NO
与柑,KVO
無法自動運作,需手動觸發(fā)蓄坏。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:@"objName"]) {
return NO;
}
return YES;
}
- (void)setObjName:(NSString *)objName
{
if (<#條件判斷##>){
[self willChangeValueForKey:@"objName"];
}
_objName = objName;
if (<#條件判斷##>) {
[self didChangeValueForKey:@"objName"];
}
}
比如我們想對objName
屬性手動出發(fā)KVO
,就在+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
中對objName
返回NO
,并在屬性-setter
方法中价捧,手動調(diào)用willChangeValueForKey
和didChangeValueForKey
(必須兩者都調(diào)用),這樣就可以實現(xiàn)手動來出發(fā)KVO
了涡戳。