Objective-C語言有一項(xiàng)特性叫做"協(xié)議"(protocol)骤公,它與Java的"接口"(interface)類似呐粘。Objective-C不支持多重繼承满俗,因而我們把某個(gè)類應(yīng)該實(shí)現(xiàn)的一系列方法定義在協(xié)議里面。協(xié)議最為常見的用途是實(shí)現(xiàn)委托模式(參見第23條)作岖,不過也有其他用法唆垃。理解并善用協(xié)議可令代碼變得更易維護(hù),因?yàn)閰f(xié)議這種方式能很好地描述接口痘儡。
"分類"(Category)也是Objective-C的一項(xiàng)重要語言特性辕万。利用分類機(jī)制,我們無須繼承子類即可直接為當(dāng)前類添加方法沉删,而在其他編程語言中渐尿,則需通過繼承子類來實(shí)現(xiàn)。由于Objective-C運(yùn)行期系統(tǒng)是高度動(dòng)態(tài)的矾瑰,所以才能支持這一特性砖茸,然而,其中也隱藏著一些陷阱殴穴,因此在使用分類之前凉夯,應(yīng)該先理解它。
對象之間經(jīng)常需要相互通信采幌,而通信方式有很多種劲够。Objective-C開發(fā)者廣泛使用一種名叫"委托模式"(Delegate pattern)的編程設(shè)計(jì)模式來實(shí)現(xiàn)對象間的通信,該模式的主旨是:定義一套接口休傍,某對象若想接受另一個(gè)對象的委托征绎,則需遵從此接口,以便成為其"委托對象"(delegate)磨取。而這"另一個(gè)對象"則可以給其委托對象回傳一些信息人柿,也可以在發(fā)生相關(guān)事件時(shí)通知委托對象柴墩。
此模式可將數(shù)據(jù)與業(yè)務(wù)邏輯解耦。比方說凫岖,用戶界面里有個(gè)顯示一系列數(shù)據(jù)所用的視圖拐邪,那么,此視圖只應(yīng)包含顯示數(shù)據(jù)所需的邏輯代碼隘截,而不應(yīng)決定要顯示何種數(shù)據(jù)以及數(shù)據(jù)之間如何交互等問題。視圖對象的屬性中汹胃,可以包含負(fù)責(zé)數(shù)據(jù)與事件處理的對象婶芭。這兩種對象分別稱為"數(shù)據(jù)源"(data source)與"委托"(delegate)。
在Objective-C中着饥,一般通過"協(xié)議"這項(xiàng)語言特性來實(shí)現(xiàn)此模式犀农,整個(gè)Cocoa系統(tǒng)框架都是這么做的。如果你的代碼也這樣寫宰掉,那么就能和系統(tǒng)框架很好地融合在一起了呵哨。
為演示此模式,我們舉個(gè)例子轨奄,假設(shè)要編寫一個(gè)從網(wǎng)上獲取數(shù)據(jù)的類孟害。此類也許要從遠(yuǎn)程服務(wù)器的某個(gè)資源里獲取數(shù)據(jù)。那么遠(yuǎn)程服務(wù)器可能過很長時(shí)間才會(huì)應(yīng)答挪拟,而在獲取數(shù)據(jù)的過程中阻塞應(yīng)用程序則是一種非常糟糕的做法挨务。于是,在這種情況下玉组,我們通常會(huì)使用委托模式: 獲取網(wǎng)絡(luò)數(shù)據(jù)的類含有一個(gè)"委托對象"谎柄,在獲取完數(shù)據(jù)之后,它會(huì)回調(diào)這個(gè)委托對象惯雳。下圖演示了此概念: EOCDataModel對象就是EOCNetworkFetcher的委托對象朝巫。EOCDataModel請求EOCNetworkFetcher"以異步方式執(zhí)行一項(xiàng)任務(wù)"(perform a task asynchronously),而EOCNetworkFetcher在執(zhí)行完這項(xiàng)任務(wù)之后石景,就會(huì)通知其委托對象劈猿,也就是EOCDataModel。
利用協(xié)議機(jī)制鸵钝,很容易就能以O(shè)bjective-C代碼實(shí)現(xiàn)此模式糙臼。上圖所演示的這種情況下,協(xié)議可以這樣來定義:
@protocol EOCNetworkFetcherDelegate
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
didReceiveData:(NSData*)data;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
didFailWithError:(NSError*)error;
@end
委托協(xié)議名通常是在相關(guān)類名后面加上Delegate一詞恩商,整個(gè)類名采用"駝峰法"來寫变逃。以這種方式來命名委托協(xié)議的話,使用此代碼的人很快就能理解其含義了怠堪。
有個(gè)這個(gè)協(xié)議之后揽乱,類就可以用一個(gè)屬性來存放其委托對象了名眉。在本例中,這個(gè)類就是EOCNetworkFetcher類凰棉。于是损拢,此類的接口可以寫成這樣:
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, weak) id <EOCNetworkFetcherDelegate> delegate;
@end
一定要注意: 這個(gè)屬性需定義成weak,而非strong撒犀,因?yàn)閮烧咧g必須為"非擁有關(guān)系"(nonowning relationship)福压。通常情況下,扮演delegate的那個(gè)對象也要持有本對象或舞。例如在本例中荆姆,想使用EOCNetworkFetcher的那個(gè)對象就會(huì)持有本對象,直到用完本對象之后映凳,才會(huì)釋放胆筒。假如聲明屬性的時(shí)候用strong將本對象與委托對象之間定為"擁有關(guān)系",那么就會(huì)引入"保留環(huán)"(retain cycle)诈豌。因此仆救,本類中存放委托對象的這個(gè)屬性要么定義成weak,要么定義成unsafe_unretained矫渔,如果需要在相關(guān)對象銷毀時(shí)自動(dòng)清空(autoniling彤蔽,參見第6條),則定義為前者蚌斩,若不需要自動(dòng)清空铆惑,則定義為后者。下圖演示了本對象與委托對象之間的所有權(quán)關(guān)系送膳。
實(shí)現(xiàn)委托對象的辦法是聲明某個(gè)類遵從委托協(xié)議员魏,然后把協(xié)議中想實(shí)現(xiàn)的那些方法在類里實(shí)現(xiàn)出來。某類若要遵從委托協(xié)議叠聋,可以在其接口中聲明撕阎,也可以在"class-continuation分類"(參見第27條)中聲明。如果要向外界公布此類實(shí)現(xiàn)了某協(xié)議碌补,那么就在接口中聲明虏束,而如果這個(gè)協(xié)議是個(gè)委托協(xié)議的話,那么通常只會(huì)在類的內(nèi)部使用厦章。所以說镇匀,這種情況一般都是在"class-continuation分類"里聲明的:
@implementation EOCDataModel () <EOCNetworkFetcherDelegate>
@end
@implementation EOCDataModel
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
didReceiveData:(NSData*)data {
/* Handle data */
}
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
didFailWithError:(NSError*)error {
/* Handle error */
}
@end
委托協(xié)議中的方法一般都是"可選的"(optional),因?yàn)榘缪?受委托者"角色的這個(gè)對象未必關(guān)心其中的所有方法袜啃。在本例中汗侵,DataModel類可能并不大關(guān)心獲取數(shù)據(jù)的過程中是否有錯(cuò)誤發(fā)生,所以此類也許不會(huì)實(shí)現(xiàn)"networkFetcher:didFailWithError:"方法。為了指明可選方法晰韵,委托協(xié)議經(jīng)常使用@optional關(guān)鍵字來標(biāo)注其大部分或全部的方法:
@protocol EOCNetworkFetcherDelegate
@optional
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
didReceiveData:(NSData*)data;
- (void)networkFetcher:(NetworkFetcher*)fetcher
didFailWithError:(EOCNSError*)error;
@end
如果要在委托對象上調(diào)用可選方法发乔,那么必須提前使用類型信息查詢方法(參見第14條)判斷這個(gè)委托對象是否能響應(yīng)相關(guān)選擇子。以EOCNetworkFetcher為例雪猪,應(yīng)該這樣寫:
NSData *data = /* data obtained from network */;
if ([_delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)]) {
[_delegate networkFetcher:self didReceiveData:data];
}
這段代碼用"respondsToSelector:"來判斷委托對象是否實(shí)現(xiàn)了相關(guān)方法栏尚。如果實(shí)現(xiàn)了,就調(diào)用只恨,如果沒實(shí)現(xiàn)译仗,就不執(zhí)行任何操作。這樣的話官觅,delegate對象就可以完全按照其需要來實(shí)現(xiàn)委托協(xié)議中的方法了古劲,不用擔(dān)心因?yàn)槟膫€(gè)方法沒實(shí)現(xiàn)而導(dǎo)致程序出問題。即便沒有設(shè)置委托對象缰猴,程序也能照常運(yùn)行,因?yàn)榻onil發(fā)送消息將使if語句的值成為false疤剑。
delegate對象中的方法名也一定要起的很恰當(dāng)才行滑绒。方法名應(yīng)該準(zhǔn)確描述當(dāng)前發(fā)生的事件以及delegate對象為何要獲知此事件。在本例中隘膘,delegate對象里的方法名讀起來非常清晰疑故,表明某個(gè)"網(wǎng)絡(luò)數(shù)據(jù)獲取器"(newwork fetcher)對象剛剛接收到某份數(shù)據(jù)。正如上一段代碼所示弯菊,在調(diào)用delegate對象中的方法時(shí)纵势,總是應(yīng)該把發(fā)起委托的實(shí)例也一并傳入方法中,這樣管钳,delegate對象在實(shí)現(xiàn)相關(guān)方法時(shí)钦铁,就能根據(jù)傳入的實(shí)例分別執(zhí)行不同的代碼了。比方說可以這樣寫:
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
didReceiveData:(NSData*)data {
if (fetcher == _myFetcherA) {
/* Handle data */
} else if (fetcher == _myFetcherB) {
/* Handle data */
}
}
上面這段代碼表明才漆,委托對象有兩個(gè)不同的"網(wǎng)絡(luò)數(shù)據(jù)獲取器"牛曹,所以它必須根據(jù)傳入的參數(shù)來判斷到底是哪個(gè)EOCNetworkFetcher獲取到了數(shù)據(jù)。若沒有此信息醇滥,則委托對象在同一時(shí)間只能使用一個(gè)網(wǎng)絡(luò)數(shù)據(jù)獲取器黎比,這么做不太好。
delegate里的方法也可以用于從獲取委托對象中獲取信息鸳玩。比方說阅虫,EOCNetworkFetcher類也許想提供一種機(jī)制: 在獲取數(shù)據(jù)時(shí)如果遇到了"重定向"(redirect),那么將詢問其委托對象是否應(yīng)該發(fā)生重定向不跟。delegate對象中的相關(guān)方法可以寫成這樣:
- (BOOL)networkFetcher:(EOCNetworkFetcher *)fetcher shouldFollowRedirectToURL:(NSURL *)url;
通過這個(gè)例子颓帝,大家應(yīng)該很容易理解此模式為何叫做"委托模式":因?yàn)閷ο蟀褢?yīng)對某個(gè)行為的責(zé)任委托給另外一個(gè)類了。
也可以用協(xié)議定義一套接口,令某類經(jīng)由該接口獲取其所需的數(shù)據(jù)躲履。委托模式的這一用法旨在向類提供數(shù)據(jù)章蚣,故而又稱"數(shù)據(jù)源模式"(Data Source Pattern)。在此模式中居暖,信息從數(shù)據(jù)源(Data Source)流向類(Class)币狠;而在常規(guī)的委托模式中,信息則從類流向受委托者(Delegate)篷帅。下圖演示了這兩條信息流史侣。
比方說,用戶界面框架中的"列表視圖"(list view)對象可能會(huì)通過數(shù)據(jù)源協(xié)議來獲取要在列表中顯示的數(shù)據(jù)魏身。除了數(shù)據(jù)源之外惊橱,列表視圖還有一個(gè)受委托者,用于處理用戶與列表的交互操作箭昵。將數(shù)據(jù)源協(xié)議與委托協(xié)議分離税朴,能使接口更加清晰,因?yàn)檫@兩部分的邏輯代碼也分開了家制。另外正林,"數(shù)據(jù)源"與"受委托者"可以是兩個(gè)不同的對象。然而一般情況下颤殴,都用同一個(gè)對象來扮演這兩種角色觅廓。
在實(shí)現(xiàn)委托模式與數(shù)據(jù)源模式時(shí),如果協(xié)議中的方法是可選的涵但,那么就會(huì)寫出一大批類似下面這樣的代碼來:
if ([_delegate respondsToSelector:@selector(someClassDidSomething:)]) {
[_delegate someClassDidSomething];
}
很容易用代碼查出某個(gè)委托對象是否能響應(yīng)特定的選擇子杈绸,可是如果頻繁執(zhí)行此操作的話,那么除了第一次檢測的結(jié)果有用之外矮瘟,后續(xù)的檢測可能都是多余的瞳脓。如果委托對象本身沒變,那么不太可能會(huì)突然響應(yīng)某個(gè)原來不能響應(yīng)的選擇子澈侠,也不太會(huì)突然無法響應(yīng)某個(gè)原來可以響應(yīng)的選擇子篡殷。鑒于此,我們通常把委托對象能否響應(yīng)某個(gè)協(xié)議方法這一信息緩存起來埋涧,以優(yōu)化程序效率板辽。假設(shè)在"網(wǎng)絡(luò)數(shù)據(jù)獲取器"那個(gè)例子中,delegate對象所遵從的協(xié)議里有個(gè)表示數(shù)據(jù)獲取進(jìn)度的回調(diào)方法棘催,每當(dāng)數(shù)據(jù)獲取有進(jìn)度時(shí)劲弦,委托對象就會(huì)得到通知。這個(gè)方法在網(wǎng)絡(luò)數(shù)據(jù)獲取進(jìn)度的回調(diào)方法醇坝,每當(dāng)數(shù)據(jù)獲取有進(jìn)度時(shí)邑跪,委托對象就會(huì)得到通知次坡。這個(gè)方法在網(wǎng)絡(luò)數(shù)據(jù)獲取器的生命期(life cycle)里會(huì)多次調(diào)用,如果每次都檢查委托對象是否能響應(yīng)此選擇子画畅,那就顯得多余了砸琅。
將剛才說的那個(gè)選擇子加入之后,delegate對象所要實(shí)現(xiàn)的委托協(xié)議就擴(kuò)充成:
@protocol EOCNetworkFetcherDelegate
@optional
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
didReceiveData:(NSData*)data;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
didFailWithError:(NSError*)error;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
didUpdateProgressTo:(float)progress;
@end
擴(kuò)充后的協(xié)議只增加了一個(gè)方法轴踱,就是可選的"networkFetcher:didUpdateProgressTo:"方法症脂。將方法響應(yīng)能力緩存起來的最佳途徑是使用"位段"(bitfield)數(shù)據(jù)類型。這是一項(xiàng)乏人問津的C語言特性淫僻,但在此處用起來卻正合適诱篷。我們可以把結(jié)構(gòu)體中某個(gè)字段所占用的二進(jìn)制位個(gè)數(shù)設(shè)為特定的值。比如像這樣:
struct data {
unsigned int fieldA : 8;
unsigned int fieldB : 4;
unsigned int fieldC : 2;
unsigned int fieldD : 1;
};
在結(jié)構(gòu)體中雳灵,fieldA位段將占用8個(gè)二進(jìn)制位棕所,fieldB占用4個(gè),fieldC占用兩個(gè)悯辙,fieldD占用1個(gè)琳省。于是,fieldA可以表示0至255之間的值躲撰,而fieldD則可以表示0或1這兩個(gè)值岛啸。我們可以像fieldD這樣,把委托對象是否實(shí)現(xiàn)了協(xié)議中的相關(guān)方法這一信息緩存起來茴肥。如果創(chuàng)建的結(jié)構(gòu)體中只有大小為1的位段,那么就能把許多Boolean值塞入一小塊數(shù)據(jù)里面了荡灾。以網(wǎng)絡(luò)數(shù)據(jù)獲取器為例瓤狐,可以在該實(shí)例中嵌入一個(gè)含有位段的結(jié)構(gòu)體作為實(shí)例變量,而結(jié)構(gòu)體中的每個(gè)位段則表示delegate對象是否實(shí)現(xiàn)了協(xié)議中的相關(guān)方法批幌。此結(jié)構(gòu)體的用法如下:
@interface EOCNetworkFetcher () {
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlags;
}
@end
筆者使用第27條所講的"class-continuation分類"來新增實(shí)例變量础锐,而新增的這個(gè)實(shí)例變量是個(gè)結(jié)構(gòu)體,其中含有三個(gè)位段荧缘,每個(gè)位段都與delegate所遵從的協(xié)議中某個(gè)可選方法相對應(yīng)皆警。在EOCNetworkFetcher類里,可以像下面這樣查詢并設(shè)置結(jié)構(gòu)體中的位段:
// Set flag
_delegateFlags.didReceiveData = 1;
// Check flag
if (_delegateFlags.didReceiveData) {
// Yes, flag set
}
這個(gè)結(jié)構(gòu)體用來緩存委托對象是否能響應(yīng)特定的選擇子截粗。實(shí)現(xiàn)緩存功能所用的代碼可以寫在delegate屬性所對應(yīng)的設(shè)置方法里:
- (void)setDelegate:(id<EOCNetworkFetcher)delegate {
_delegate = delegate;
_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
_delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];
_delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}
這樣的話信姓,每次調(diào)用delegate的相關(guān)方法之前,就不用檢測委托對象是否能響應(yīng)給定的選擇子了绸罗,而是直接查詢結(jié)構(gòu)體里的標(biāo)志:
if (_delegateFlags.didUpdateProgressTo) {
[_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
}
在相關(guān)方法要調(diào)用很多次時(shí)意推,值得進(jìn)行這種優(yōu)化。而是否需要優(yōu)化珊蟀,則應(yīng)依照具體代碼來定菊值。這就需要分析代碼性能,并找出瓶頸,若發(fā)現(xiàn)執(zhí)行速度需要改進(jìn)腻窒,則可使用此技巧昵宇。如果要頻繁通過數(shù)據(jù)源協(xié)議從數(shù)據(jù)源中獲取多份相互獨(dú)立的數(shù)據(jù),那么這項(xiàng)優(yōu)化技術(shù)極有可能會(huì)提高程序效率儿子。
要點(diǎn)
- 委托模式為對象提供了一套接口瓦哎,使其可由此將相關(guān)事件告知其他對象。
- 將委托對象應(yīng)該支持的接口定義成協(xié)議典徊,在協(xié)議中把可能需要處理的事件定義成方法杭煎。
- 當(dāng)某對象需要從另外一個(gè)對象中獲取數(shù)據(jù)時(shí),可以使用委托模式卒落。這種情況下羡铲,該模式亦稱"數(shù)據(jù)源協(xié)議"(data source protocol)。
- 若有必要儡毕,可實(shí)現(xiàn)含有位段的結(jié)構(gòu)體也切,將委托對象是否能響應(yīng)相關(guān)協(xié)議方法這一信息緩存至其中。