寫在前面
程序設(shè)計(jì)語言中有各種各樣的設(shè)計(jì)模式(pattern)和與此對應(yīng)的反設(shè)計(jì)模式(anti-pattern),譬如singleton、factory、observer、MVC等等。對于基于Objective-C的iOS開發(fā)而言,有些設(shè)計(jì)模式幾乎已經(jīng)成為開發(fā)環(huán)境的一部分,譬如MVC,自打我們設(shè)計(jì)第一個(gè)頁面開始就已經(jīng)開始與之打交道了;KVO,即Key-Value Observing(根據(jù)我的理解它屬于observer設(shè)計(jì)模式)也一樣,只是它已經(jīng)成為Objective-C事實(shí)標(biāo)準(zhǔn)了,作為一個(gè)iOS開發(fā)者,必須對它有相當(dāng)?shù)牧私狻?/p>
之前對KVO的了解僅限于使用層面,沒有去想過它是如何實(shí)現(xiàn)的,更沒有想過它會存在一些坑;甚至在剛接觸它時(shí),會盡可能創(chuàng)造機(jī)會使用它,譬如監(jiān)聽UITextField的text值的變化;但近幾天接觸了Objective-C的Runtime相關(guān)的知識,從runtime層面了解到了KVO的實(shí)現(xiàn)原理(即KVO的「消息轉(zhuǎn)發(fā)機(jī)制」),也通過閱讀各位大神的博客了解到了它的坑。
本文首先分析KVO和Runtime的關(guān)系,闡述KVO的實(shí)現(xiàn)原理;然后結(jié)合大神們的博客整理KVO存在的坑以及避免掉坑的正確使用姿勢。
關(guān)于KVO,即Key-Value Observing,官方文檔《Key-Value Observing Programming Guide》里的介紹比較簡短明了:
Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
KVO的實(shí)現(xiàn)
KVO的實(shí)現(xiàn)也依賴于Objective-C的Runtime,官方文檔《Key-Value Observing Programming Guide》中在《Key-Value Observing Implementation Details》部分簡單提到它的實(shí)現(xiàn):
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的實(shí)現(xiàn):
當(dāng)你觀察一個(gè)對象(稱該對象為「被觀察對象」)時(shí),一個(gè)新的類會動態(tài)被創(chuàng)建。這個(gè)類繼承自「被觀察對象」所對應(yīng)類的,并重寫該被觀察屬性的setter方法;針對setter方法的重寫無非是在賦值語句前后加上相應(yīng)的通知(或曰方法調(diào)用);最后,把「被觀察對象」的isa指針(isa指針告訴Runtime系統(tǒng)這個(gè)對象的類是什么)指向這個(gè)新創(chuàng)建的中間類,對象就神奇變成了新創(chuàng)建類的實(shí)例。
根據(jù)文檔的描述,雖然被觀察對象的isa指針被修改了,但是調(diào)用其class方法得到的類信息仍然是它之前所繼承類的類信息,而不是這個(gè)新創(chuàng)建類的類信息。
補(bǔ)充:下面對isa指針和類方法class作以更多的說明。
isa指針和類方法class的返回值都是Class類型,如下:
@interfaceNSObject {
ClassisaOBJC_ISA_AVAILABILITY;
}
+ (Class)class;
根據(jù)我的理解,一般情況下,isa指針和class方法返回值都是一樣的;但KVO底層實(shí)現(xiàn)時(shí),動態(tài)創(chuàng)建的類只是重寫了被觀察屬性的setter方法,并未重寫類方法class,因此向被觀察者發(fā)送class消息實(shí)際上仍然調(diào)用的是被觀察者原先類的類方法+ (Class)class,得到的類型信息當(dāng)然是原先類的類信息,根據(jù)我的猜測,isKindOfClass:和isMemberOfClass:與class方法緊密相關(guān)。
國外的大神Mike Ash早在2009年就做了關(guān)于KVO的實(shí)現(xiàn)細(xì)節(jié)的探究,更多詳細(xì)參考這里。
KVO的槽點(diǎn)
AFNetworking作者M(jìn)attt Thompson在《Key-Value Observing》中說:
Ask anyone who’s been around the NSBlock a few times: Key-Value Observing has the worst API in all of Cocoa.
另一位不認(rèn)識的大神在《KVO Considered Harmful》中也寫道:
KVO, or key-value observing, is a pattern that Cocoa provides for us for subscribing to changes to the properties of other objects. It’s hands down the most poorly designed API in all of Cocoa, and even when implemented perfectly, it’s still an incredibly dangerous tool to use, reserved only for when no other technique will suffice.
總之,兩位大神都認(rèn)為KVO的API非常差勁!
其中《KVO Considered Harmful》中對KVO的槽點(diǎn)有了比較詳細(xì)的闡述,這一部分內(nèi)容就取材于此。
為了更好說明這些槽點(diǎn),假設(shè)一個(gè)應(yīng)用場景:ZWTableViewController繼承自UITableViewController,它現(xiàn)在需要做一件事情,即監(jiān)測自己的tableView的contentSize,現(xiàn)采用典型的方式(即KVO)處理這么個(gè)需求。
所有的observe處理都放在一個(gè)方法里
對于上述示例「監(jiān)聽self.tableView的contentSize變化」,最基本處理方式是:
// register observer
- (void)viewDidLoad {
[superviewDidLoad];
[_tableView addObserver:selfforKeyPath:@"contentSize"options:0context:NULL];
/* ... */
}
// 處理observe
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
[selfconfigureView];
}
但考慮到observeValueForKeyPath:ofObject:change:context:中可能會很多其他的observe事務(wù),所以observeValueForKeyPath:ofObject:change:context:更好的邏輯是:
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
if(object == _tableView && [keyPath isEqualToString:@"contentSize"]) {
[selfconfigureView];
}
}
但如果KVO處理的事情種類多且繁雜,這會造成observeValueForKeyPath:ofObject:change:context:代碼特別長,極不優(yōu)雅。
嚴(yán)重依賴于string
KVO嚴(yán)重依賴string,換句話說,KVO中的keyPath必須是NSString這個(gè)事實(shí)使得編譯器沒辦法在編譯階段將錯(cuò)誤的keyPath給找出來;譬如很容易將「contentSize」寫成「contentsize」;
需要自己處理superclass的observe事務(wù)
對于Objective-C,很多時(shí)候runtime系統(tǒng)都會自動幫助處理superclass的方法。譬如對于dealloc,假設(shè)類Father繼承自NSObject,而類Son繼承自Father,創(chuàng)建一個(gè)Son類對象aSon,在aSon被釋放的時(shí)候,runtime會先調(diào)用Son的dealloc實(shí)例方法,之后會自動調(diào)用Father的dealloc實(shí)例方法,而無需在Son的dealloc中顯式執(zhí)行[super dealloc];。但對于KVO不會這樣,所以為了保證父類(父類可能也會自己observe處理嘛)的observe事務(wù)也能被處理,上述observeValueForKeyPath:ofObject:change:context:代碼得改成這樣:
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
if(object == _tableView && [keyPath isEqualToString:@"contentSize"]) {
[selfconfigureView];
}else{
[superobserveValueForKeyPath:keyPath ofObject:object change:change context:context];
}
多次相同的removeObserver會導(dǎo)致crash
寫過KVO代碼的人都知道,對同一個(gè)對象執(zhí)行兩次removeObserver操作會導(dǎo)致程序crash。
在同一個(gè)文件中執(zhí)行兩次相同的removeObserver屬于粗心,比較容易debug出來;但是跨文件執(zhí)行兩次相同的removeObserver就不是那么容易發(fā)現(xiàn)了。
我們一般會在dealloc中進(jìn)行removeObserver操作(這也是Apple所推薦的)。
譬如,假設(shè)上述的ZWTableViewController的父類UITableViewController也對tableView的contentSize注冊了相同的監(jiān)聽;那么UITableViewController的dealloc中常常會寫出如下這樣的代碼:
[_tableView removeObserver:selfforKeyPath:@"contentSize"context:NULL];
按照一般習(xí)慣,ZWTableViewController中的dealloc也會有相同的處理;那么當(dāng)ZWTableViewController對象被釋放時(shí),ZWTableViewController的dealloc和其父類UITableViewController的dealloc都被調(diào)用,這樣會導(dǎo)致相同的removeObserver被執(zhí)行兩次,自然會導(dǎo)致crash。
《KVO Considered Harmful》中還有很多其他的槽點(diǎn),《Key-Value Observing Done Right》也描述了一些,這里就不多說了,更多信息還是建議看原文。
不過好在上述的槽點(diǎn)「嚴(yán)重依賴于string」和「多次相同的removeObserver會導(dǎo)致crash」有比較好的解決方案,如下『使用KVO』所述。
使用KVO
訂閱
KVO中與訂閱相關(guān)的API只有一個(gè):
-(void)addObserver:(NSObject*)observer
forKeyPath:(NSString*)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context;
對于這四個(gè)參數(shù):
observer: The object to register for KVO notifications. The observer must implement the key-value observing methodobserveValueForKeyPath:ofObject:change:context:.
keyPath: The key path, relative to the receiver, of the property to observe. This value must not be nil.
options: A combination of the NSKeyValueObservingOptions values that specifies what is included in observation notifications. For possible values, seeNSKeyValueObservingOptions.
context: Arbitrary data that is passed to observer inobserveValueForKeyPath:ofObject:change:context:.
大神們認(rèn)為這個(gè)API丑陋的重要原因是因?yàn)楹竺鎯蓚€(gè)參數(shù):options和context。
下面來對這兩個(gè)參數(shù)進(jìn)行詳細(xì)介紹。
options
options可選值是一個(gè)NSKeyValueObservingOptions枚舉值,到目前為止,一共包括四個(gè)值,在介紹這四個(gè)值各自表示的意思之前,先得有一個(gè)概念,即KVO響應(yīng)方法有一個(gè)NSDictionary類型參數(shù)change(下面『響應(yīng)』中可以看到),這個(gè)字典中會有一個(gè)與被監(jiān)聽屬性相關(guān)的值,譬如被改變之前的值、新值等,NSDictionary中有啥值由『訂閱』時(shí)的options值決定,options可取值如下:
NSKeyValueObservingOptionNew: 指示change字典中包含新屬性值;
NSKeyValueObservingOptionOld: 指示change字典中包含舊屬性值;
NSKeyValueObservingOptionInitial: 相對復(fù)雜一些,NSKeyValueObserving.h文件中有詳細(xì)說明,此處略過;
NSKeyValueObservingOptionPrior: 相對復(fù)雜一些,NSKeyValueObserving.h文件中有詳細(xì)說明,此處略過;
現(xiàn)在細(xì)想,options這個(gè)參數(shù)也忒復(fù)雜了,難怪大神們覺得這個(gè)API丑陋(不過我等小民之前從未想過這個(gè)問題,=_=,沒辦法,Apple是個(gè)大帝國,我只是其中一個(gè)跪舔的小屁民)。
不過更糟心的是下面的context參數(shù)。
context
options信息量稍大,但其實(shí)蠻好理解的,然而對于context,在寫這篇博客之前,一直不知道context參數(shù)有啥用(也沒在意)。
context作用大了去了,在上文『KVO的槽點(diǎn)』提到一個(gè)槽點(diǎn)『多次相同的removeObserver會導(dǎo)致crash』。導(dǎo)致『多次調(diào)用相同的removeObserver』一個(gè)很重要的原因是我們經(jīng)常在addObserver時(shí)為context參數(shù)賦值NULL,關(guān)于如何使用context參數(shù),下面的『響應(yīng)』中會提到。
響應(yīng)
iOS的UI交互(譬如UIButton的一次點(diǎn)擊)有一個(gè)非常不錯(cuò)的消息轉(zhuǎn)發(fā)機(jī)制 — Target-Action模型,簡單來說,為指定的event指定target和action處理方法。
UIButton*button = [UIButtonnew];
[button addTarget:selfaction:@selector(buttonDidClicked:) forControlEvents:UIControlEventTouchUpInside];
這種target-action模型邏輯非常清晰。作為對比,KVO的響應(yīng)處理就非常糟糕了,所有的響應(yīng)都對應(yīng)是同一個(gè)方法- (void)observeValueForKeyPath:ofObject:change:context:,其原型如下:
-(void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void *)context;
除了NSDictionary類型參數(shù)change之外,其余幾個(gè)參數(shù)都能在–addObserver:forKeyPath:options:context:找到對應(yīng)。
change參數(shù)上文已經(jīng)講過了,這里不多說了。下面將針對「嚴(yán)重依賴于string」和「多次相同的removeObserver會導(dǎo)致crash」這兩個(gè)槽點(diǎn)對keyPath和context參數(shù)進(jìn)行闡述。
keyPath
keyPath的類型是NSString,這導(dǎo)致了我們使用了錯(cuò)誤的keyPath而不自知,譬如將@”contentSize”錯(cuò)誤寫成@”contentsize”,一個(gè)更好的方法是不直接使用@”xxxoo”,而是積極使用NSStringFromSelector(SEL aSelector)方法,即改@"contentSize"為NSStringFromSelector(@selector(contentSize))。
context
對于context,上文已經(jīng)提到一種場景:假如父類(設(shè)為ClassA)和子類(設(shè)為ClassB)都監(jiān)聽了同一個(gè)對象腫么辦?是ClassB處理呢還是交給父類ClassA的observeValueForKeyPath:ofObject:change:context:處理呢?更復(fù)雜一點(diǎn),如果子類的子類(設(shè)為ClassC)也監(jiān)聽了同一個(gè)對象,當(dāng)ClassB接收到ClassC的[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];消息時(shí)又該如何處理呢?
這么一想,KVO的API還真的是設(shè)計(jì)非常糟糕。一般來說,比較靠譜的做法是自己的屁股自己擦。ClassB的observe事務(wù)在ClassB中處理,怎么知道是自己的事務(wù)還是ClassC傳上來的事務(wù)呢?用context參數(shù)判斷!
在addObserver時(shí)為context參數(shù)設(shè)置一個(gè)獨(dú)一無二的值即可,在responding處理時(shí)對這個(gè)context值進(jìn)行檢驗(yàn)。如此就解決了問題,但這需要靠用戶(各個(gè)層級類的程序員用戶)自覺遵守。
取消訂閱
和『訂閱』以及『響應(yīng)』不同,『取消訂閱』有兩個(gè)方法:
-(void)removeObserver:(NSObject*)observer forKeyPath:(NSString*)keyPath context:(void *)context;
-(void)removeObserver:(NSObject*)observer forKeyPath:(NSString*)keyPath;
個(gè)人覺得應(yīng)該盡可能使用第一個(gè)方法,保持『訂閱』-『響應(yīng)』-『取消訂閱』一致性嘛,養(yǎng)成好習(xí)慣!
此外,為了避免『取消訂閱』時(shí)造成的crash,可以把『取消訂閱』代碼放在@try-@catch語句中,如下是一個(gè)比較全面的的KVO使用示例:
staticvoid* zwContentSize = &zwContentSize;
- (void)viewDidLoad {
[superviewDidLoad];
// 1. subscribe
[_tableView addObserver:self
forKeyPath:NSStringFromSelector(@selector(contentSize))
options:NSKeyValueObservingOptionNew
context:zwContentSize];
}
// 2. responding
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
if(context == zwContentSize) {
// configure view
}else{
[superobserveValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc {
@try{
// 3. unsubscribe
[_tableView removeObserver:self
forKeyPath:NSStringFromSelector(@selector(contentSize))
context:zwContentSize];
}
@catch(NSException*exception) {
}
}
總之,KVO很強(qiáng)大,但也挺坑,使用它要養(yǎng)成好習(xí)慣,避免入坑!