第 23 條:通過(guò)委托與數(shù)據(jù)源協(xié)議進(jìn)行對(duì)象間通信
Objective-C 可以使用 “委托模式”(Delegate pattern)的編程設(shè)計(jì)模式來(lái)實(shí)現(xiàn)對(duì)象間的通信:定義一套接口,某對(duì)象若想接受另一個(gè)對(duì)象的委托,則需遵從此接口,以便成為其 “委托對(duì)象”(delegate)。Objective-C 一般利用 “協(xié)議” 機(jī)制來(lái)實(shí)現(xiàn)此模式。
定義協(xié)議:
@protocol EOCNetworkingFetcherDelegate
@optional
- (void)newworkingFetcher:(EOCNetworkingFetcher *)fetcher
didRecevieData:(NSData *)data; - (void)newworkingFetcher:(EOCNetworkingFetcher *)fetcher
didFailWithError:(NSError *)error;
@end
@interface EOCNetworkingFetcher : NSObject
@property (nonatomic,weak) id<EOCNetworkingFetcherDelegate> delegate;
@end
委托協(xié)議名通常時(shí)在相關(guān)的類名加上Delegate 一詞,也是采用 “駝峰法” 來(lái)命名。
類可以用一個(gè)屬性存放其委托對(duì)象,屬性要用weak 來(lái)修飾,避免產(chǎn)生 “保留環(huán)”(retain cycle)。
某類若要遵從某委托協(xié)議,可以在其接口中聲明,也可以在"class-continuation 分類" 中聲明,如果要向外界公布此類實(shí)現(xiàn)了某協(xié)議,就在接口中聲明,如果這個(gè)協(xié)議是個(gè)委托協(xié)議,通常只會(huì)在這個(gè)類的內(nèi)部使用,這樣子就在分類中聲明就好了。
- 如果要在委托對(duì)象上調(diào)用可選方法,那么必須提前使用類型信息查詢方法,判斷這個(gè)委托對(duì)象能否響應(yīng)相關(guān)的選擇子。
NSData *data;
if([_delegate respondsToSelector:@selector(networkFetcher:didRecevieData:)]){
[_delegate networkFetcher:self didRecevieData:data];
}
在調(diào)用delegate 對(duì)象中的方法時(shí),總應(yīng)該把發(fā)起委托的實(shí)例也一并傳入方法中,這樣子,delegate 對(duì)象在實(shí)現(xiàn)相關(guān)方法時(shí),就能根據(jù)傳入的實(shí)例分別執(zhí)行不同的代碼了。
delegate 里的方法也可以用于從委托對(duì)象中獲取信息(數(shù)據(jù)源模式)。
在實(shí)現(xiàn)委托模式和數(shù)據(jù)源模式的時(shí),協(xié)議中的方法是可選的,我們就會(huì)寫出大量這種判斷代碼:
if([_delegate respondsToSelector:@selector(networkFetcher:didRecevieData:)]){
[_delegate networkFetcher:self didRecevieData:data];
}
每次調(diào)用方法都會(huì)判斷一次,其實(shí)除了第一次檢測(cè)的結(jié)構(gòu)有用,后續(xù)的檢測(cè)很有可能都是多余的,因?yàn)槲袑?duì)象本身沒(méi)變,不太可能會(huì)一下子不響應(yīng),一下子響應(yīng)的,所以我們這里可以把這個(gè)委托對(duì)象能否響應(yīng)某個(gè)協(xié)議方法記錄下來(lái),以優(yōu)化程序效率。
將方法響應(yīng)能力緩存起來(lái)的最佳途徑是使用 “位段”(bitfield)數(shù)據(jù)類型。我們可以把結(jié)構(gòu)體中某個(gè)字段所占用的二進(jìn)制位個(gè)數(shù)設(shè)為特定的值。
位段,C語(yǔ)言允許在一個(gè)結(jié)構(gòu)體中以位為單位來(lái)指定其成員所占內(nèi)存長(zhǎng)度,這種以位為單位的成員稱為“位段”或稱“位域”( bit field) 。
struct data {
unsigned int filedA : 8;
unsigned int filedB : 4;
unsigned int filedC : 2;
unsigned int filedD : 1;
}
filedA 位段占用8個(gè)二進(jìn)制位,filedB 位段占用4個(gè)二進(jìn)制位,filedC 位段占用2個(gè)二進(jìn)制位,filedD位段占用1個(gè)二進(jìn)制位。filedA 就可以表示0至255之間的值,而filedD 則可以表示0或1這兩個(gè)值。
我們可以像filedD 這樣子,創(chuàng)建大小只有1的位段,這樣子就可以把Boolean 值塞入這一小塊數(shù)據(jù)里面,這里很適合這樣子做。
利用位段就可以清楚的表示delegate 對(duì)象是否能響應(yīng)協(xié)議中的方法。
@interface EOCNetworkingFetcher ()
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlags
@end
//使用
//set flag
_delageteFlags.didReceiveData = 1;
//check flag
if(_delageteFlags.didReceiveData){
//YES
}
可以在delegate 屬性的設(shè)置方法里面寫實(shí)現(xiàn)緩存功能所用的代碼。
這樣子,每次調(diào)用delegate 的相關(guān)方法之前,就不用檢測(cè)委托對(duì)象是否能響應(yīng)給定的選擇子了,而是直接查詢結(jié)構(gòu)體里面的標(biāo)志。
在相關(guān)方法需要調(diào)用很多次時(shí),就要思考是否有必要進(jìn)行優(yōu)化,分析代碼性能,找出瓶頸,使用這個(gè)位段這個(gè)技術(shù)可以提供執(zhí)行速度。
委托模式為對(duì)象提供了一套接口,使其可由此將相關(guān)事件告知其他對(duì)象。
將委托對(duì)象應(yīng)該支持的接口定義成協(xié)議,在協(xié)議中把可能需要處理的事件定義成方法。
當(dāng)某對(duì)象需要從另外一個(gè)對(duì)象中獲取數(shù)據(jù)時(shí),可以使用委托模式。這種情境下,該模式亦稱 “數(shù)據(jù)源協(xié)議”(data source protocal)。
若有必要,可實(shí)現(xiàn)含有位段的結(jié)構(gòu)體,將委托對(duì)象是否能響應(yīng)相關(guān)協(xié)議方法這一信息緩存至其中。
第 24 條:將類的實(shí)現(xiàn)代碼分散到便于管理的數(shù)個(gè)分類之中
一個(gè)類經(jīng)常有很多方法,盡管代碼寫的比較規(guī)范,這個(gè)文件還是會(huì)越來(lái)越大,定位問(wèn)題以及閱讀上都會(huì)造成不便。我們可以通過(guò) “分類” 機(jī)制來(lái)把代碼按邏輯劃分到幾個(gè)分區(qū)中。
通過(guò)分類機(jī)制,可以把類代碼分成很多個(gè)易于管理的小塊,以便單獨(dú)檢視。
可以考慮創(chuàng)建Private 分類,將一些不是公共API 的方法,隱藏起來(lái)。寫程序庫(kù)的時(shí)候,加上不暴露頭文件,使用者就不知道庫(kù)里還有這些私有方法。
使用分類機(jī)制把類的實(shí)現(xiàn)代碼劃分成易于管理的小塊。
將應(yīng)該視為 “私有” 的方法歸入為叫Private 的分類中,以隱藏實(shí)現(xiàn)細(xì)節(jié)。
第 25 條:總是為第三方類的分類名稱加前綴
分類機(jī)制常用于向無(wú)源碼的既有類中新增新功能,但是在使用的時(shí)候要十分小心,不然很容易產(chǎn)生Bug。因?yàn)檫@個(gè)機(jī)制時(shí)在運(yùn)行期系統(tǒng)加載分類時(shí),將其方法直接加到原類中,這里要注意方法重名的問(wèn)題,不然會(huì)覆蓋原類中的同名方法。
一般用前綴來(lái)區(qū)分各個(gè)分類的名稱與其中所定義的方法。
不要輕易去利用分類來(lái)覆蓋方法,這里需要慎重考慮。
向第三方類中添加分類時(shí),總應(yīng)該給其名稱加上你專用的前綴。
向第三方類中添加分類時(shí),總應(yīng)給其中的方法名加上你專用的前綴
第 26 條:勿在分類中聲明屬性
可以利用運(yùn)行期的關(guān)聯(lián)對(duì)象機(jī)制,為分類聲明屬性,但是這種做法要盡量避免,因?yàn)槌?"class-continuation 分類" 之外,其他分類都無(wú)法向類中新增實(shí)例變量,因此,他們無(wú)法把實(shí)現(xiàn)屬性所需的實(shí)例變量合成出來(lái)。
在分類定義屬性的時(shí)候,會(huì)報(bào)警告,表明此分類無(wú)法合成該屬性相關(guān)的實(shí)例變量,所以開發(fā)者需要在分類中為該屬性實(shí)現(xiàn)存取方法。
利用關(guān)聯(lián)對(duì)象機(jī)制可以解決分類中不能合成實(shí)例變量的問(wèn)題。自己實(shí)現(xiàn)存取方法,但是要注意該屬性的內(nèi)存管理語(yǔ)義(屬性特質(zhì))。
@property (nonatomic,copy) NSString *name;
static const void *kViewControllerName = &kViewControllerName;
(void)setName:(NSString *)name {
objc_setAssociatedObject(self, kViewControllerName, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}(NSString *)name {
NSString *myName = objc_getAssociatedObject(self, kViewControllerName);
return myName;
}
在可以修改源代碼的情況下,盡量把屬性定義在主接口中,這里是唯一能夠定義實(shí)例變量的地方,屬性只是定義實(shí)例變量及相關(guān)存取方法所用的 “語(yǔ)法糖”。
由于實(shí)現(xiàn)屬性所需的全部方法都已實(shí)現(xiàn),所以不會(huì)再為該屬性自動(dòng)合成實(shí)例變量了。
盡量把封裝數(shù)據(jù)所用的全部屬性都定義在主接口里。
在 “class-continuation 分類” 之外的其他分類中,可以定義存取方法,但盡量不要定義屬性。
第 27 條:使用 ”class-continuation 分類“ 隱藏實(shí)現(xiàn)細(xì)節(jié)
- ”class-continuation 分類“ 必須定義在本身類的實(shí)現(xiàn)文件中,而且這里是唯一可以聲明實(shí)例變量的分類,而且此分類沒(méi)有特定的實(shí)現(xiàn)文件,這個(gè)分類也沒(méi)有名字。這里可以定義實(shí)例變量的原因是 “ 穩(wěn)固的ABI” 機(jī)制,我們無(wú)須知道對(duì)象的大小就可以直接使用它。
@interface EOCPerson ()
@end
可以將不需要要暴露給外界知道的實(shí)例變量及方法寫在 “class-continuation 分類” 中。
編寫Objective-C++ 代碼時(shí)候,使用 “class-continuation 分類” 會(huì)十分方便。因?yàn)閷?duì)于引用了C++的文件的實(shí)現(xiàn)文件需要用.mm 為擴(kuò)展名,表示編譯器應(yīng)該將此文件按照Objective-C++ 來(lái)編譯。C++ 類必須完全引入,編譯器要完整地解析其定義才能得知這個(gè)C++ 對(duì)象的實(shí)例變量大小。如果把對(duì)C++ 類的引用寫在頭文件的話,其他引用到這個(gè)類也會(huì)引用到這個(gè)C++ 類,就也需要編譯成Objective-C++ 才行,這樣子很容易失控。
這里可以利用 “class-continuation 分類” 把引用C++ 類的細(xì)節(jié)寫到實(shí)現(xiàn)文件中,這樣子別的類引用這個(gè)類就不會(huì)受到影響,甚至都不知道這個(gè)類底層實(shí)現(xiàn)混有C++ 代碼。
使用 “class-continuation 分類” 還可以將頭文件聲明 “只讀” 的屬性擴(kuò)展成 “可讀寫”,以便在類的內(nèi)部可以設(shè)置其值。
我們通常不直接訪問(wèn)實(shí)例變量,而是通過(guò)設(shè)置方法來(lái)做,因?yàn)檫@樣子可以觸發(fā) “鍵值觀測(cè)” (Key-Value Observing,KVO)通知。
若對(duì)象所遵循的協(xié)議只應(yīng)視為私有,也可以同過(guò)“class-continuation 分類” 來(lái)隱藏。
通過(guò) “class-continuation 分類” 向類中新增實(shí)例變量。
如果某屬性在主接口中聲明為 “只讀”,而類的內(nèi)部又要用設(shè)置方法修改此屬性,那么就在 “class-continuation 分類” 中將其擴(kuò)展為 “可讀寫”。
把私有方法的原型聲明在 “class-contiunation 分類” 里面。
若想使類所遵循的協(xié)議不為人所知,則可于 “class-contiunation 分類” 中聲明。
第 28 條:通過(guò)協(xié)議提供匿名對(duì)象
@property (nonatomic,weak) id<EOCDelegate> delegate;
該屬性類型是id<EOCDelegate> 的,所以實(shí)際上任何類的都能充當(dāng)這一屬性,即便該類不繼承NSObject 也可以,只要遵循EOCDelegae 協(xié)議就可以了,對(duì)于具備此屬性的類來(lái)說(shuō),delegate 就是 “匿名的”。
協(xié)議可在某種程度上提供匿名類型。具體的對(duì)象類型可以淡化成遵從某協(xié)議的id 類型,協(xié)議里規(guī)定了對(duì)象所應(yīng)實(shí)現(xiàn)的方法。
使用匿名對(duì)象來(lái)隱藏類型名稱(或類名)。
如果具體類型不重要,重要的是對(duì)象能夠響應(yīng)(定義在協(xié)議里的)特定方法,那么可使用匿名對(duì)象來(lái)表示。
內(nèi)存管理
第 29 條:理解引用計(jì)數(shù)
引用計(jì)數(shù)工作原理
Objective-C 語(yǔ)言使用引用計(jì)數(shù)來(lái)管理內(nèi)存,每個(gè)對(duì)象都有個(gè)可以遞增遞減的計(jì)數(shù)器,用以表示當(dāng)前有多少個(gè)事物想令此對(duì)象繼續(xù)存活下去。
NSObject 協(xié)議聲明下面三個(gè)方法用于操作計(jì)數(shù)器,以遞增或遞減其值:
retain 遞增保留計(jì)數(shù)
release 遞減保留計(jì)數(shù)
autorelease 待稍后清理 “自動(dòng)釋放池” 時(shí),再遞減保留計(jì)數(shù)
在調(diào)用release 之后,對(duì)象所占的內(nèi)存可能會(huì)被回收,這樣子在調(diào)用對(duì)象的方法就可能使程序崩潰,這里 “可能” 的意思是對(duì)象所占的內(nèi)存在 “解除分配” (deallocated)之后,只是放回 “可用內(nèi)存池”(avaiable pool)。若果執(zhí)行方法時(shí)尚未覆寫對(duì)象,那么對(duì)象仍然有效。
為避免在不經(jīng)意間使用無(wú)效對(duì)象,一般在調(diào)用完release 之后都會(huì)清空指針,保證不會(huì)出現(xiàn)可能指向無(wú)效對(duì)象的指針,這種指針通常被稱為 “懸掛指針”(dangling pointer)。
自動(dòng)釋放池
調(diào)用release 會(huì)立刻遞減對(duì)象的保留計(jì)數(shù)(這里可能會(huì)令系統(tǒng)回收此對(duì)象),調(diào)用autorelease 方法,是在稍后遞減計(jì)數(shù),通常是在下一次 “事件循環(huán)” 時(shí)遞減。
此特性很有用,尤其是在返回對(duì)象時(shí)更應(yīng)該用它
- (NSString *)stringValue {
NSString *str = [[NSString alloc]
initWithFormat:@"I am this %@",self];
return str;
}
這里返回的str 對(duì)象的保留計(jì)數(shù)會(huì)比期望值多1,因?yàn)檎{(diào)用alloc 會(huì)令保留計(jì)數(shù)+1,這里又沒(méi)有對(duì)應(yīng)的釋放操作,這樣子就意味著調(diào)用者要負(fù)責(zé)處理這多出來(lái)的保留操作。在這個(gè)方法又不能釋放str,否則還沒(méi)等方法返回,str 這個(gè)對(duì)象就被釋放了。這里應(yīng)該用autorelease ,它會(huì)在稍后釋放對(duì)象,保證這里可以保證調(diào)用者可以先用這個(gè)str 對(duì)象。
- autorelease 能延長(zhǎng)對(duì)象聲明周期,使其在跨越方法調(diào)用邊界后依然可以存活一段時(shí)間。
保留環(huán)
呈環(huán)狀相互引用的多個(gè)對(duì)象,相互持有,這將導(dǎo)致內(nèi)存泄漏,這里循環(huán)中的對(duì)象其保留計(jì)數(shù)不會(huì)降為0。
通常采用 “弱引用” 來(lái)解決此問(wèn)題,或者從外界命令某個(gè)對(duì)象不再保留另外一個(gè)對(duì)象來(lái)打破保留環(huán),從而避免內(nèi)存泄漏。
引用計(jì)數(shù)機(jī)制通過(guò)可以遞增遞減的計(jì)數(shù)器來(lái)管理內(nèi)存。對(duì)象創(chuàng)建好之后,其保留計(jì)數(shù)至少為1。若保留計(jì)數(shù)為正,則對(duì)象繼續(xù)存活。當(dāng)保留計(jì)數(shù)降為0時(shí),對(duì)象就被銷毀了。
在對(duì)象生命期中,其余對(duì)象通過(guò)引用來(lái)保留或釋放此對(duì)象。保留與釋放操作分別會(huì)遞增及遞減保留計(jì)數(shù)。
第 30 條:以 ARC 簡(jiǎn)化引用計(jì)數(shù)
內(nèi)存泄漏:沒(méi)有正確的釋放已經(jīng)不再使用的內(nèi)存。
自用引用計(jì)數(shù)預(yù)先加入適當(dāng)?shù)谋A艋蜥尫挪僮鱽?lái)避免內(nèi)存泄漏,使用ARC 時(shí),引用計(jì)數(shù)實(shí)際上還是要執(zhí)行的,只是保留與釋放操作是由ARC 自動(dòng)添加的。
ARC 會(huì)自動(dòng)執(zhí)行retain、release、autorelease、dealloc等操作,所以在ARC 下調(diào)用這些內(nèi)存管理方法是非法的。因?yàn)锳RC 會(huì)分析何處應(yīng)該自動(dòng)調(diào)用內(nèi)存管理方法,所以我們?cè)偈謩?dòng)調(diào)用的話,會(huì)干擾其工作。
實(shí)際上,ARC 在調(diào)用這些方法時(shí),并不是普通的Objective-C 消息派發(fā)機(jī)制,而是直接調(diào)用其底層的C 語(yǔ)言函數(shù),這樣子性能會(huì)更好。
使用ARC 時(shí)必須遵循的方法命名規(guī)則
- 將內(nèi)存管理語(yǔ)義在方法名中表示出來(lái),若方法名以下列詞語(yǔ)開頭,則返回的對(duì)象歸
調(diào)用者所有:
alloc
new
copy
mutableCopy
- 將內(nèi)存管理交由編譯器和運(yùn)行期組件來(lái)做,可以使代碼得到多種優(yōu)化。
變量的內(nèi)存管理語(yǔ)義
ARC 也會(huì)處理局部變量與實(shí)例變量的內(nèi)存管理。
我們通常會(huì)給局部變量加上修飾符來(lái)打破 “塊”(block)所引入的 “保留環(huán)”(retain cycle)。
ARC 如何清理實(shí)例變量
對(duì)實(shí)例變量進(jìn)行內(nèi)存管理,必須在 “回收分配給對(duì)象的內(nèi)存” 時(shí)生成必要的清理代碼。凡事具備強(qiáng)引用的變量,都必須釋放,ARC 會(huì)在dealloc 方法中插入這些代碼。
ARC 會(huì)借用Objective-C++ 的一項(xiàng)特性來(lái)生成清理代碼,在回收對(duì)象時(shí),待回收對(duì)象會(huì)調(diào)用所有C++ 對(duì)象的析構(gòu)函數(shù),編譯器如果發(fā)現(xiàn)某個(gè)對(duì)象里含有C++ 對(duì)象,就會(huì)生成名為.cxx_desteuct 的方法,ARC 借助此特性,在該方法中生成清理內(nèi)存所需的代碼。
對(duì)于非Objective-C 的對(duì)象,然后需要我們手動(dòng)清理。CFRelease();
覆寫內(nèi)存管理方法
非ARC 時(shí)可以覆寫內(nèi)存管理方法,在ARC 下禁止覆寫內(nèi)存管理方法,會(huì)干擾到ARC 分析對(duì)象生命周期的工作。
有ARC 之后,程序員就無(wú)需擔(dān)心內(nèi)存管理問(wèn)題了。使用ARC 來(lái)編程,可省去類中的許多 “樣板代碼”。
ARC 管理對(duì)象生命周期的辦法基本上是:在適合的地方插入 “保留” 及 “釋放” 操作。在ARC 環(huán)境下,變量的內(nèi)存管理語(yǔ)義可以通過(guò)修飾符指明,而原來(lái)則需要手工執(zhí)行 “保留”及 “釋放” 操作。
ARC 只負(fù)責(zé)管理Objective-C 對(duì)象的內(nèi)存。尤其要注意:CoreFoundation 對(duì)象不歸ARC 管理,開發(fā)者必須適時(shí)調(diào)用CFRetain/CFRelease。
第 31 條:在 dealloc 方法中只釋放引用并解除監(jiān)聽
對(duì)象在經(jīng)歷生命周期后,最終會(huì)為系統(tǒng)回收,這時(shí)候就要執(zhí)行dealloc 方法。每個(gè)對(duì)象生命周期內(nèi),此方法只會(huì)調(diào)用一次,也就是保留計(jì)數(shù)為0 的時(shí)候,絕對(duì)不能自己調(diào)用dealloc 方法,運(yùn)行期會(huì)在適當(dāng)?shù)臅r(shí)候調(diào)用,一旦調(diào)用,對(duì)象就不再有效了,后續(xù)的方法調(diào)用均是無(wú)效的。
dealloc 方法主要是釋放對(duì)象所擁有的引用,也就是把Objective-C 對(duì)象都釋放掉,ARC 會(huì)通過(guò)自動(dòng)生成的.cxx_desteuct 方法,在dealloc 中為你自動(dòng)添加這些釋放代碼。但是其他非Objective-C 對(duì)象就需要自己手動(dòng)釋放了。
dealloc 方法通常還需要把原來(lái)配置過(guò)的觀測(cè)行為都清理掉,例如通知等。
對(duì)于開銷較大或者系統(tǒng)內(nèi)稀缺的資源不應(yīng)該等到dealloc 才清理(文件描述符、套接字、大塊內(nèi)存等),因?yàn)閐ealloc 并不會(huì)在特定的時(shí)機(jī)調(diào)用,因?yàn)橛锌赡苓€有別的對(duì)象持有它。應(yīng)該自己實(shí)現(xiàn)一個(gè)方法,當(dāng)應(yīng)用程序用完資源對(duì)象后,就調(diào)用此方法,這樣子對(duì)象的生命周期就更加明確了。
調(diào)用dealloc 方法的那個(gè)線程會(huì)執(zhí)行 “最終的釋放操作”,令對(duì)象保留計(jì)數(shù)為0,而某些方法必須在特定的線程調(diào)用,若在dealloc 中調(diào)用那么方法,無(wú)法保證當(dāng)前的線程就是那個(gè)方法所需的線程。在dealloc 里盡量不要去調(diào)用方法,包括屬性的存取方法,因?yàn)樵谶@些方法可能會(huì)被覆寫,并在其中做一些無(wú)法在回收階段安全執(zhí)行的操作。
在dealloc 方法里,應(yīng)該做的事情就是釋放指向其他對(duì)象的引用,并取消原來(lái)訂閱的 “鍵值觀測(cè)”(KVO)或NSNotification 等通知,不要做其他事情。
如果對(duì)象持有文件描述符等系統(tǒng)資源,那么應(yīng)該專門編寫一個(gè)方法來(lái)釋放此種資源。這樣的類要和其使用者約定:用完資源后必須調(diào)用close 方法。
執(zhí)行異步任務(wù)的方法不應(yīng)在dealloc 里調(diào)用;只有在正常狀態(tài)下執(zhí)行的那些方法也不應(yīng)在dealloc 里調(diào)用,因?yàn)榇藭r(shí)對(duì)象已處于回收的狀態(tài)。
第 32 條:編寫 “異常安全代碼” 時(shí)留意內(nèi)存管理問(wèn)題
純C 中沒(méi)有異常,C++與Objective-C 都支持異常,在運(yùn)行期系統(tǒng)中C++與Objective-C 異常相互兼容,也就是說(shuō),從其中一門語(yǔ)言里拋出的異常能用另外一門語(yǔ)言所編寫的 “異常處理程序” 來(lái)捕獲。
Objective-C 錯(cuò)誤模型表明,異常只應(yīng)發(fā)生嚴(yán)重錯(cuò)誤后拋出,發(fā)生異常如何管理內(nèi)存很重要,在try 塊中保留某個(gè)對(duì)象的,但是在釋放它之前拋出異常了,這時(shí)候就無(wú)法正常釋放了,這時(shí)候需要借助@finally 塊來(lái)保證釋放對(duì)象的代碼一定會(huì)執(zhí)行,且只執(zhí)行一次。
在ARC 不會(huì)自動(dòng)生成處理異常中的代碼,因?yàn)檫@樣子需要加入大量的樣板代碼,以便追蹤待清理的對(duì)象,從而在拋出異常時(shí)將其釋放。可以這段代碼會(huì)嚴(yán)重運(yùn)行期的性能,還會(huì)增加應(yīng)用程序的大小。
可以通過(guò)-fobjc-arc-exceptions 這個(gè)編譯編織來(lái)開啟這個(gè)功能,但是這個(gè)功能不應(yīng)該作為生成這種安全處理異常所用的附加代碼,應(yīng)該是讓代碼處于Objective-C++模式。
捕獲異常時(shí),一定要注意將try 塊內(nèi)創(chuàng)建的對(duì)象清理干凈。
在默認(rèn)情況下,ARC 不生成安全處理異常所需的清理代碼。開啟編譯標(biāo)志后,可生成這種代碼,不過(guò)會(huì)導(dǎo)致應(yīng)用程序變大,而且會(huì)降低運(yùn)行效率。
第 33 條:以弱引用避免保留環(huán)
幾個(gè)對(duì)象都已某種方式互相引用,從而形成 “環(huán)”,這種情況通常會(huì)泄漏內(nèi)存,因?yàn)闆](méi)有東西引用環(huán)中對(duì)象,這樣子環(huán)里的對(duì)象互相引用,不會(huì)被系統(tǒng)回收。
避免保留環(huán)的最佳方式就是弱引用,來(lái)表示 “非擁有關(guān)系”,unsafe_unretained、weak 修飾都是可以達(dá)到的。unsafe_unretained 表示屬性值可能不安全,有可能系統(tǒng)把屬性所指的對(duì)象回收了,但是這個(gè)屬性依然指向那塊地址,那么再調(diào)用它的方法可能會(huì)使程序崩潰,用weak 修飾的時(shí)候,在所指對(duì)象被回收的時(shí)候,會(huì)將屬性的指針置為nil。
一般來(lái)說(shuō),如果不擁有某對(duì)象,就不要保留它,這條規(guī)則對(duì)collection 例外,collection 雖然不直接擁有其內(nèi)容,但是它要代表自己所屬的那個(gè)對(duì)象來(lái)保留這些元素。
將某些引用設(shè)為weak,可避免出現(xiàn) “保留環(huán)”。
weak 引用可以自動(dòng)清空,也可以不自動(dòng)清空。自動(dòng)清空是隨著ARC 而引入的新特性,由運(yùn)行期系統(tǒng)來(lái)實(shí)現(xiàn)。在具備自動(dòng)清空功能的弱引用上,可以隨意讀取其數(shù)據(jù),因?yàn)檫@種引用不會(huì)指向已經(jīng)回收過(guò)的對(duì)象。
第 34 條:以 “自動(dòng)釋放池塊” 降低內(nèi)存峰值
- 釋放對(duì)象有兩種方式:
一種是調(diào)用release 方法,使其保留計(jì)數(shù)立即遞減
一種是調(diào)用autorelease 方法,將對(duì)象放入 “自動(dòng)釋放池” 中,自動(dòng)釋放池用于存放那些需要稍后某個(gè)時(shí)刻釋放的對(duì)象,清空(drain)自動(dòng)釋放池時(shí),系統(tǒng)會(huì)向其中的對(duì)象發(fā)送release 消息。
- 創(chuàng)建自動(dòng)釋放池,系統(tǒng)會(huì)自動(dòng)創(chuàng)建一些線程,這些線程默認(rèn)都有自動(dòng)釋放池,每次執(zhí)行 “事件循環(huán)”時(shí),都會(huì)將其清空。自動(dòng)釋放池于左邊花括號(hào)創(chuàng)建,并于對(duì)應(yīng)的右花括號(hào)自動(dòng)清空。位于自動(dòng)釋放池范圍內(nèi)的對(duì)象,會(huì)在末尾處受到release 消息。
@autoreleasepool {
//...
}
內(nèi)存峰值:是指應(yīng)用程序在某個(gè)特定時(shí)段內(nèi)的最大內(nèi)存用量。
對(duì)象有可能會(huì)放在自動(dòng)釋放池里面,需要等到線程執(zhí)行下一次事件循環(huán)才會(huì)清空,這里會(huì)導(dǎo)致應(yīng)用程序所占內(nèi)存會(huì)持續(xù)增加,等到臨時(shí)對(duì)象釋放的時(shí)候,內(nèi)存用量又會(huì)突然下降。我們現(xiàn)在就想把這個(gè)內(nèi)存峰值給降低下來(lái)。
可以增加一個(gè)自動(dòng)釋放池來(lái)解決這個(gè)問(wèn)題:這樣子對(duì)象就會(huì)加入到這個(gè)釋放池,而不是線程的主池中,每次循環(huán)都創(chuàng)建和釋放這個(gè)釋放池。
for (int i = 0;i < 100000;i++){
@autorelease{
NSObject *object = [NSObject new];
}
}
自動(dòng)釋放池機(jī)制就像 “棧” 一樣。系統(tǒng)創(chuàng)建好自動(dòng)釋放池之后,就將其推入棧中,而清空自動(dòng)釋放池,則相當(dāng)于將其從棧中彈出。在對(duì)象上執(zhí)行自動(dòng)釋放操作,就等于將其放入棧頂?shù)哪莻€(gè)池。
對(duì)于是否需要用池來(lái)優(yōu)化效率,這個(gè)得考慮清楚來(lái),因?yàn)樽詣?dòng)釋放池的創(chuàng)建還是有一丟丟開銷的,所以盡量不要建立額外的自動(dòng)釋放池。
自動(dòng)釋放池排布在棧中,對(duì)象收到autorelease 消息后,系統(tǒng)將其放入到最頂端的池里。
合理運(yùn)用自動(dòng)釋放池,可降低應(yīng)用程序的內(nèi)存峰值。
@autoreleasepool 這種新式寫法能創(chuàng)建出更為輕便的自動(dòng)釋放池。
第 35 條:用 “僵尸對(duì)象” 調(diào)試內(nèi)存管理問(wèn)題
向已回收的對(duì)象發(fā)送消息是不安全的,是否崩潰這個(gè)是看對(duì)象所占的內(nèi)存有沒(méi)有為其他內(nèi)容所覆寫。
Cocoa 提供 “僵尸對(duì)象”(Zombie Object)這個(gè)非常方便的功能,開啟后,運(yùn)行期系統(tǒng)會(huì)把已經(jīng)回收的實(shí)例轉(zhuǎn)換成特殊的 “僵尸對(duì)象”,而不會(huì)真正回收它們。這個(gè)對(duì)象所在的核心內(nèi)無(wú)法重用,因此不可能遭到覆寫,僵尸對(duì)象收到消息后,會(huì)拋出異常。
使用:Xcode Scheme 中的Enable Zombie Objects 選項(xiàng),打開會(huì)將NSZombieEnabled 環(huán)境變量設(shè)成YES。
系統(tǒng)在即將回收時(shí),會(huì)執(zhí)行一個(gè)附加步驟,將對(duì)象轉(zhuǎn)換成僵尸對(duì)象,而不徹底回收。僵尸類是從名為NSZombie 的模版類復(fù)制出來(lái)的。NSZombie 類并未實(shí)現(xiàn)任何方法,此類沒(méi)有超類,因此跟NSObject 一樣,也是一個(gè) "根類",該類只有一個(gè)實(shí)例變量,叫做isa,所以發(fā)給他的消息都要經(jīng)過(guò) “完整的消息轉(zhuǎn)發(fā)機(jī)制” 。
在完整的消息轉(zhuǎn)發(fā)機(jī)制中,forwarding 是核心,檢查接受消息的對(duì)象所屬的類名,若是NSZombie ,則表示消息接受者是僵尸對(duì)象,需要特殊處理。
系統(tǒng)在回收對(duì)象時(shí),可以不將其真的回收,而是把它轉(zhuǎn)化成僵尸對(duì)象。通過(guò)環(huán)境變量NSZombieEnabled 可開啟此功能。
系統(tǒng)會(huì)修改對(duì)象的isa 指針,令其指向特殊的僵尸類,從而使該對(duì)象變成僵尸對(duì)象。僵尸類能夠響應(yīng)所有的選擇子,響應(yīng)方式為:打印一條包含消息內(nèi)容及其接受者的消息,然后終止應(yīng)用程序。
第 36 條:不要使用 retainCount
每個(gè)對(duì)象都有一個(gè)計(jì)數(shù)器,其表明還有多少個(gè)其他對(duì)象想令此對(duì)象繼續(xù)存活。在ARC retainCount 這個(gè)方法已經(jīng)廢棄了,但是在非ARC 中也不應(yīng)該調(diào)用這個(gè)方法,因?yàn)檫@個(gè)保留計(jì)數(shù)只是返回某個(gè)時(shí)間點(diǎn)的值,并不會(huì)聯(lián)系上下文給出真正有用的值。
retainCount 可能永遠(yuǎn)不返回0,因?yàn)橄到y(tǒng)有時(shí)候會(huì)優(yōu)化對(duì)象的釋放行為,在保留計(jì)數(shù)為1的時(shí)候就把它回收了。
不應(yīng)該依靠保留計(jì)數(shù)的具體址來(lái)編碼。
對(duì)象的保留計(jì)數(shù)看似有用,實(shí)則不然,因?yàn)槿魏谓o定時(shí)間點(diǎn)上的 “絕對(duì)保留計(jì)數(shù)”(absolute retain count)都無(wú)法反映對(duì)象生命期的全貌。
引入ARC 之后,retainCount 方式就正式廢止了,在ARC 下調(diào)用方法會(huì)導(dǎo)致編譯器報(bào)錯(cuò)。
塊與大中樞派發(fā)
第 37 條:理解 “塊” 這一概念
塊可以實(shí)現(xiàn)閉包。
塊的基礎(chǔ)知識(shí)
- 塊用 “^” 符號(hào)來(lái)表示,后面跟著一對(duì)花括號(hào),括號(hào)里面是塊的實(shí)現(xiàn)代碼。塊其實(shí)就是個(gè)值,而且自有其相關(guān)類型,可以賦值給變量;塊類型的語(yǔ)法和函數(shù)指針類似。
^{
//block implementation herer
}
//這里定義了名為someBlock 的變量
//塊類型的語(yǔ)法結(jié)構(gòu)如下
//return_type (^block_name)(parameters)
void (^someBlock)() = ^{
//block implementation herer
}
在聲明塊的范圍內(nèi),所有變量都可以被其捕獲。默認(rèn)情況下被塊捕獲的變量是不可以在塊里修改的,不過(guò)可以在聲明變量的時(shí)候加上__block 修飾符,這樣子就可以在塊內(nèi)修改了。
如果塊所捕獲的變量是對(duì)象類型,那么就會(huì)自動(dòng)保留它,在系統(tǒng)釋放這個(gè)塊的時(shí)候,也會(huì)將其一并釋放。
塊總能修改實(shí)例變量,所以在聲明時(shí)無(wú)須加__block。不過(guò)如果通過(guò)讀取或?qū)懭氩僮鞑东@了實(shí)例變量,那么也會(huì)自動(dòng)把self 變量一并捕獲了,因?yàn)閷?shí)例變量是與self 所指代的實(shí)例關(guān)聯(lián)在一起的。
塊的內(nèi)部結(jié)構(gòu)
- 塊本身也是對(duì)象,在存放塊對(duì)象的內(nèi)存區(qū)域中,首個(gè)變量是指向Class 對(duì)象的指針(isa 指針)。
- invoke 變量是這個(gè)函數(shù)指針,指向塊的實(shí)現(xiàn)代碼。函數(shù)原型至少要接受一個(gè)void* 型的參數(shù),此參數(shù)代表塊。為什么要把塊對(duì)象作為參數(shù)傳進(jìn)來(lái)呢,因?yàn)樵趫?zhí)行塊的時(shí)候,要從內(nèi)存中把這些捕獲到的變量讀出來(lái)。
descriptor 變量是指向結(jié)構(gòu)體的指針,這個(gè)結(jié)構(gòu)體包含塊的一些信息。
全局塊、棧塊及堆塊
- 定義塊的時(shí)候,其所占的內(nèi)存區(qū)域是分配在棧中,意思就是,塊只在定義它的那個(gè)范圍內(nèi)有效。
void (^block)();
if(***){
block = ^(){
NSLog(@"Block A");
};
}else{
block = ^(){
NSLog(@"Block B");
};
}
block();
/*定義在if else 語(yǔ)句中的兩個(gè)塊都分配在棧內(nèi)存中,編譯器會(huì)給每個(gè)塊分配好棧內(nèi)存,然而等離開了相應(yīng)的范圍之后,編譯器有可能把分配給塊內(nèi)存覆寫掉。所以這里執(zhí)行block() 有危險(xiǎn)。
為了解決這個(gè)問(wèn)題,可以給塊發(fā)送copy 消息以拷貝之。這樣子的話,就可以把塊從棧復(fù)制到堆可。一旦復(fù)制到堆上,塊就成了帶引用計(jì)數(shù)的對(duì)象了,后續(xù)的復(fù)制操作都不會(huì)真的執(zhí)行復(fù)制,只是遞增塊對(duì)象的引用計(jì)數(shù)。
*/
- 全局塊聲明在全局內(nèi)存里,而且也不能被系統(tǒng)回收,相當(dāng)于單例。由于運(yùn)行該塊所需的全部信息在編譯期確定,所以可以把它作為全局塊,這是一種優(yōu)化技術(shù):若把如此簡(jiǎn)單的塊當(dāng)成復(fù)雜的塊來(lái)處理,那就會(huì)在復(fù)制及丟棄該塊時(shí)執(zhí)行一些無(wú)謂的操作。
塊是C、C++、Objective-C 中的詞法閉包。
塊可接受參數(shù),也可返回值。
塊可以分配在棧和堆上,也可以是全局的。分配在棧上的塊可以拷貝到堆里,這樣的話,就和標(biāo)準(zhǔn)的Objective-C 對(duì)象一樣,具備引用計(jì)數(shù)了。
第 38 條:為常用的塊類型創(chuàng)建 typedef
每個(gè)塊都具備其 “ 固定類型”,因而可將其賦值給適當(dāng)類型的變量。
由于塊類型的語(yǔ)法比較復(fù)雜難記,我們可以給塊類型起個(gè)別名。用C 語(yǔ)言中的 “ 類型定義” 的特性。typedef 關(guān)鍵字用于給類型起個(gè)易讀的別名。
typedef int(^EOCSomeBlock)(BOOL flag, int value);
EOCSomeBlock block = ^(BOOL flag, int value){
//to do
};
以typedef 重新定義塊類型,可令塊變量用起來(lái)更加簡(jiǎn)單。
定義新類型時(shí)應(yīng)遵從現(xiàn)有的命名習(xí)慣,勿使其名稱與別的類型向沖突。
不妨為同一個(gè)塊簽名定義多個(gè)類型別名。如果要重構(gòu)的代碼使用了塊類型的某個(gè)別名,那么只需要修改相應(yīng)depedef 中的塊簽名即可,無(wú)須改動(dòng)其他typedef。
第 39 條:用handle 塊降低代碼分散程度
場(chǎng)景:異步方法執(zhí)行完任務(wù),需要以某種手段通知相關(guān)代碼。經(jīng)常使用的技巧是設(shè)計(jì)一個(gè)委托協(xié)議,令關(guān)注此事件的對(duì)象遵從該協(xié)議,對(duì)象成了delegate 之后,就可以在相關(guān)事件發(fā)生時(shí)得到通知了。
使用塊來(lái)寫的話,代碼會(huì)更清晰,使得代碼更加緊致。
在創(chuàng)建對(duì)象時(shí),可以使用內(nèi)聯(lián)的handle 塊將相關(guān)業(yè)務(wù)邏輯一并聲明。
在有多個(gè)實(shí)例需要監(jiān)控時(shí),如果采用委托模式,那么經(jīng)常需要根據(jù)傳入的對(duì)象來(lái)切換,若改用handle 塊來(lái)實(shí)現(xiàn),則可直接將塊與相關(guān)對(duì)象放在一起。
設(shè)計(jì)API 時(shí)如果用到handle 塊,那么可以增加一個(gè)參數(shù),使調(diào)用者可通過(guò)此參數(shù)來(lái)決定應(yīng)該把塊安排在哪個(gè)隊(duì)列上執(zhí)行。
第 40 條:用塊引用其所屬對(duì)象時(shí)不要出現(xiàn)保留環(huán)
如果塊所捕獲的對(duì)象直接或間接地保留了塊本身,那么就得當(dāng)心保留環(huán)問(wèn)題。
一定要找個(gè)合適的時(shí)機(jī)解除保留環(huán),而不能把責(zé)任推給API 的調(diào)用者。
第 41 條:多用派發(fā)隊(duì)列,少用同步鎖
- 如果有多個(gè)線程要執(zhí)行同一份代碼,那么有時(shí)可能會(huì)出問(wèn)題,這種情況下,通常要使用鎖來(lái)實(shí)現(xiàn)某種同步機(jī)制。在GCD 出現(xiàn)之前,有兩種辦法:
采用內(nèi)置的 “同步塊”(synchronization block)
- (void)synchronizedMethod {
@synchronized(self){
//safe
}
}
/*
這種寫法會(huì)根據(jù)給定的對(duì)象,自動(dòng)創(chuàng)建一個(gè)鎖,并等待塊中的代碼執(zhí)行完畢,執(zhí)行到代碼結(jié)尾,鎖就釋放了。
但是,濫用 @synchronized(self) 則會(huì)降低代碼效率,因?yàn)楣灿猛粋€(gè)鎖的那些同步塊,都必須按順序執(zhí)行。
*/
直接使用NSLock 對(duì)象,也可以使用NSRecursiveLock “遞歸鎖”,線程能多次持有該鎖,而且不會(huì)出現(xiàn)死鎖。
_lock = [[NSLock alloc] init];
- (void)synchronizedMethod {
[_lock lock];
//safe
[_lock unlock];
}
對(duì)于上面兩種方法,有些缺陷,同步塊會(huì)導(dǎo)致死鎖,直接使用鎖對(duì)象,遇到死鎖,就會(huì)非常麻煩。
GCD 以更簡(jiǎn)單、更高效的形式為代碼加鎖。
例子:屬性是開發(fā)者經(jīng)常需要同步的地方,可以使用atomic 特質(zhì)來(lái)修飾屬性,來(lái)保證其原子性,每次肯定可以從中獲取到有效值,然而在同一個(gè)線程上多次調(diào)用獲取方法(getter),每次獲取到結(jié)果未必相同,在兩次訪問(wèn)操作之間,其他線程可能會(huì)寫入新的屬性值。
使用 “串行同步隊(duì)列”,將讀取操作及寫入操作都安排在同一個(gè)隊(duì)列里,即可保證數(shù)據(jù)同步。
_syncQueue = dispatch_queue_create("com.pengxuyuan.syncQueue", NULL);
(NSString *)name {
__block NSString *tempName;
dispatch_sync(_syncQueue, ^{
tempName = _name;
});
return tempName;
}(void)setName:(NSString *)name {
dispatch_sync(_syncQueue, ^{
_name = name;
});
}
/*
上面是用串行同步隊(duì)列來(lái)保證數(shù)據(jù)同步:把設(shè)置操作與獲取操作都安排在序列化的隊(duì)列里執(zhí)行,這樣子,所有針對(duì)屬性的訪問(wèn)操作都是同步的了。
*/
/*
進(jìn)一步優(yōu)化,設(shè)置方法不一定非得是同步的,因?yàn)椴恍枰祷刂怠_@樣子可以提高設(shè)置方法的執(zhí)行速度,而讀取操作與寫入操作依然會(huì)按照順序執(zhí)行。
但是這里可能發(fā)現(xiàn)這種寫法比原來(lái)慢,因?yàn)閳?zhí)行異步派發(fā)時(shí),需要拷貝塊。
*/
- (void)setName:(NSString *)name {
dispatch_async(_syncQueue, ^{
_name = name;
});
}
/*
我們現(xiàn)在目的就是要做到:多個(gè)獲取方法可以并發(fā)執(zhí)行,而獲取方法與設(shè)置方法不能并發(fā)執(zhí)行。
我們還可以使用并發(fā)隊(duì)列來(lái)實(shí)現(xiàn),現(xiàn)在都是在并發(fā)隊(duì)列上面執(zhí)行任務(wù),但是順序不能控制,我們可以用柵欄(barrier)來(lái)解決。
這兩個(gè)函數(shù)可以向隊(duì)列派發(fā)塊,將其作為柵欄來(lái)使用:
dispatch_barrier_sync(dispatch_queue_t queue,^(void)block)
dispatch_barrier_async(dispatch_queue_t queue,^(void)block)
在隊(duì)列中,柵欄塊必須單獨(dú)執(zhí)行,不能與其他塊并行,這只對(duì)并發(fā)隊(duì)列有意義,因?yàn)榇嘘?duì)列中的塊總是按照順序逐個(gè)執(zhí)行的。并發(fā)隊(duì)列如果發(fā)現(xiàn)接下來(lái)要處理的塊是柵欄塊,那么就一直要等到當(dāng)前所有的并發(fā)塊都執(zhí)行完畢,才會(huì)單獨(dú)執(zhí)行這個(gè)柵欄塊。執(zhí)行完?yáng)艡趬K,再按照正常方式向下處理。
*/
-----> 現(xiàn)在并發(fā)隊(duì)列 還不能滿足要求
_syncQueue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
(NSString *)name {
__block NSString *tempName;
dispatch_sync(_syncQueue1, ^{
tempName = _name;
});
return tempName;
}(void)setName:(NSString *)name {
dispatch_async(_syncQueue1, ^{
_name = name;
});
}
-----> 轉(zhuǎn)換寫法 用柵欄塊控制屬性的設(shè)置方法 不能并行
_syncQueue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
(NSString *)name {
__block NSString *tempName;
dispatch_sync(_syncQueue1, ^{
tempName = _name;
});
return tempName;
}(void)setName:(NSString *)name {
dispatch_barrier_async(_syncQueue1, ^{
_name = name;
});
}
派發(fā)隊(duì)列可用來(lái)表述同步語(yǔ)義(synchronization semantic),這種做法要比使用@synchronized 塊或則NSLock 對(duì)象更簡(jiǎn)單。
將同步與異步派發(fā)結(jié)合起來(lái),可以實(shí)現(xiàn)與普通加鎖機(jī)制一樣的同步行為,而這么做卻不會(huì)阻塞執(zhí)行異步派發(fā)的線程。
使用同步隊(duì)列及柵欄塊,可以使同步行為更加高效。
第 42 條:多用GCD,少用performSelector 方法
performSelector 可以任意調(diào)用方法,還可以延遲調(diào)用,還可以指定運(yùn)行方法所用的線程。
但是如果是動(dòng)態(tài)來(lái)調(diào)用performSelector 方法的時(shí)候,編譯器都不知道執(zhí)行的選擇子是什么,必須到了運(yùn)行期才能確定,這種情況在ARC 下會(huì)報(bào)警告,因?yàn)榫幾g器不知道方法名,所以不能運(yùn)用ARC 內(nèi)存管理規(guī)則來(lái)判定返回值是否應(yīng)該釋放,對(duì)于這種情況ARC 不會(huì)幫我們添加任何釋放操作。
performSelector 方法調(diào)用的時(shí)候?qū)τ诜祷仡愋椭荒苁莢oid或?qū)ο箢愋停瑢?duì)于有返回值的需要自己做多次轉(zhuǎn)換,對(duì)于參數(shù)的也最多只能傳2個(gè),介于此performSelector 還是比較不方便的。
對(duì)于performSelector 遇到的問(wèn)題,我們都可以用GCD 解決。
performSeletor 系列方法在內(nèi)存管理方面容易有疏忽。它無(wú)法確定將要執(zhí)行的選擇子具體是什么,因而ARC 編譯器也就無(wú)法插入適當(dāng)?shù)膬?nèi)存管理方法。
performSeletor 系列方法所能處理的選擇子太過(guò)局限了,選擇子的返回類型及發(fā)送給方法的參數(shù)個(gè)數(shù)收到限制。
如果想把任務(wù)放在另一個(gè)線程上執(zhí)行,那么最好不要用performSeletor 系列方法,而是應(yīng)該把任務(wù)封裝到塊里,然后調(diào)用大中樞派發(fā)機(jī)制的相關(guān)方法來(lái)實(shí)現(xiàn)。
第 43 條:掌握GCD 及操作隊(duì)列的使用時(shí)機(jī)
在執(zhí)行后臺(tái)任務(wù)時(shí),GCD 不一定是最佳方式,還有一種技術(shù)叫做NSOperationQueue,開發(fā)者可以把操作以NSOperation 子類的形式放在隊(duì)列中,而這些操作也可以并發(fā)執(zhí)行。
GCD 是純C 的API,操作隊(duì)列的則是Objective-C 的對(duì)象。用NSOperationQueue 類的“addOperationWithBlock” 方法搭配NSBlockOperation 類操作隊(duì)列,其語(yǔ)法與純GCD 方式非常類似。使用NSOperation 及NSOperationQueue 的好處如下:
取消某個(gè)操作。如果使用操作隊(duì)列,那么想取消操作是很容易的。運(yùn)行任務(wù)之前,可以在NSOperation 對(duì)象調(diào)用cancel 方法,該方法會(huì)設(shè)置對(duì)象內(nèi)的標(biāo)識(shí)位,用以表明此任務(wù)不需執(zhí)行,不過(guò),已經(jīng)啟動(dòng)的任務(wù)無(wú)法取消。若不是操作隊(duì)列,而是把塊安排到GCD 隊(duì)列,那就無(wú)法取消了。那套架構(gòu)是 “安排好任務(wù)之后就不管了”。開發(fā)者可以在應(yīng)用層自己來(lái)實(shí)現(xiàn)取消功能,不過(guò)這樣子做需要編寫很多代碼,而那些代碼其實(shí)已經(jīng)由操作隊(duì)列實(shí)現(xiàn)好了。
指定操作間的依賴關(guān)系。一個(gè)操作可以依賴其他多個(gè)操作。開發(fā)者能夠指定操作之間的依賴關(guān)系,使特定的操作必須在另外一個(gè)操作順序執(zhí)行完畢方可執(zhí)行,比方說(shuō),從服務(wù)器下載并處理文件的動(dòng)作,可以用操作來(lái)表示,而在處理其他文件之前,必須先下載 “清單文件”。后續(xù)的下載操作,都要依賴于先下載清單文件這一操作。如果操作隊(duì)列允許并發(fā)的話,那么后續(xù)的多個(gè)下載操作就可以同時(shí)執(zhí)行,但前提是它們所依賴的那個(gè)清單文件下載操作已經(jīng)執(zhí)行完畢。
通過(guò)鍵值觀測(cè)機(jī)制監(jiān)控NSOperation 對(duì)象的屬性。NSOperation 對(duì)象有許多屬性都適合通過(guò)鍵值觀測(cè)機(jī)制(KVO)來(lái)監(jiān)聽,比如可以通過(guò)isCancalled 屬性來(lái)判斷任務(wù)是否取消。如果想在某個(gè)任務(wù)變更期狀態(tài)時(shí)得到通知,或是想用比GCD 更為精細(xì)的方式來(lái)控制所要執(zhí)行的任務(wù),那么鍵值觀測(cè)機(jī)制會(huì)很有用。
制定操作的優(yōu)先級(jí)。操作的優(yōu)先級(jí)表示此操作與隊(duì)列其他操作之間的優(yōu)先關(guān)系。優(yōu)先級(jí)高的操作先執(zhí)行,優(yōu)先級(jí)低的后執(zhí)行。操作隊(duì)列的調(diào)度算法已經(jīng)比較成熟。反之,GCD 則沒(méi)有直接實(shí)現(xiàn)此功能的辦法,GCD 的隊(duì)列有優(yōu)先級(jí),但是是針對(duì)整個(gè)隊(duì)列來(lái)說(shuō)的,而不是針對(duì)每個(gè)塊來(lái)說(shuō)的。對(duì)于優(yōu)先級(jí)這一點(diǎn),操作隊(duì)列所提供的功能比GCD 更為便利。
重用NSOperation 對(duì)象。系統(tǒng)內(nèi)置類一些NSOperation 的子類供開發(fā)者調(diào)用,要是不想用這些固有子類的話,那就得自己來(lái)創(chuàng)建了。這些類就是普通的Objective-C 對(duì)象,能夠存放任何信息。對(duì)象在執(zhí)行時(shí)可以充分利用存于其中的信息,而且還可以隨意調(diào)用定義在類中的方法。這比派發(fā)隊(duì)列中哪些簡(jiǎn)單的塊要強(qiáng)大。這些NSOperation 類可以在代碼中多次使用。
在解決多線程與任務(wù)管理問(wèn)題時(shí),派發(fā)隊(duì)列并非唯一方案。
操作隊(duì)列提供了一套高層的Objective-C API,能實(shí)現(xiàn)純GCD 所具備的絕大部分功能,而且還能完成一些更為復(fù)雜的操作,那些操作若改用GCD 來(lái)實(shí)現(xiàn),則需另外編寫代碼。
第 44 條:通過(guò)Dispatch Group 機(jī)制,根據(jù)系統(tǒng)資源狀況來(lái)執(zhí)行任務(wù)
一系列任務(wù)可歸入一個(gè)dispatch group 之中。開發(fā)者可以在這組任務(wù)執(zhí)行完畢時(shí)獲得通知。
通過(guò)dispatch group,可以在并發(fā)式派發(fā)隊(duì)列里同時(shí)執(zhí)行多項(xiàng)任務(wù)。此時(shí)GCD 會(huì)根據(jù)系統(tǒng)資源狀況來(lái)調(diào)度這些并發(fā)執(zhí)行的任務(wù)。開發(fā)者若自己實(shí)現(xiàn)此功能,則需編寫大量代碼。
第 45 條:使用dispatch_once 來(lái)執(zhí)行只需運(yùn)行一次的線程安全代碼
- 對(duì)于單例我們創(chuàng)建唯一實(shí)例,之前都是用@synchronized 加鎖來(lái)解決多線程的問(wèn)題,GCD 提供了一個(gè)更加簡(jiǎn)單的方法來(lái)實(shí)現(xiàn)。
//單例
+(instancetype)shareInstance{
static dispatch_once_t onceToken;
static PXYAdvertisingPagesHelper *shareInstance;
dispatch_once(&onceToken, ^{
shareInstance = [PXYAdvertisingPagesHelper new];
shareInstance.adTimeout = 5.0;
});
return shareInstance;
}
- 使用dispatch_once 可以簡(jiǎn)化代碼,并且徹底保證線程安全。
經(jīng)常需要編寫 “只需執(zhí)行一次的線程安全代碼”。通常使用GCD 所提供的dispatch_once 函數(shù),很容易就能實(shí)現(xiàn)此功能。
標(biāo)記應(yīng)該聲明在static 或 global 作用域中,這樣的話,在把只需執(zhí)行一次的快傳給dispatch_once 函數(shù)時(shí),傳進(jìn)去的標(biāo)記也是相同的。
第 46 條:不要使用dispatch_get_current_queue
Mac OS X 與 iOS 的UI 事務(wù)都需要在主線程上執(zhí)行,而這個(gè)線程就相當(dāng)于GCD 中的主隊(duì)列。
dispatch_get_current_queue 這個(gè)函數(shù)返回當(dāng)前正在執(zhí)行代碼的隊(duì)列,但是在iOS 6.0版本起,已經(jīng)棄用這個(gè)函數(shù)了。
該函數(shù)有種典型的錯(cuò)誤用法,就是用它檢測(cè)當(dāng)前隊(duì)列是不是某個(gè)特定的隊(duì)列,試圖以此來(lái)避免執(zhí)行同步派發(fā)時(shí)可能遭遇到的死鎖問(wèn)題。
看下下面這個(gè)代碼:
dispatch_queue_t queueA = dispatch_queue_creat("com.pengxuyuan.queueA",NULL);
dispatch_queue_t queueB = dispatch_queue_creat("com.pengxuyuan.queueB",NULL);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dipatch_sync(queueA, ^{
//DeadLock
});
});
});
//這里是個(gè)典型的死鎖現(xiàn)象,queueA 串行隊(duì)列上面的同步任務(wù)相互等待了。
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^();
if(dispatch_get_current_queue() == queueA){
block();
}else{
dipatch_sync(queueA, block);
}
});
});
//但是用dispatch_get_current_queue 這個(gè)來(lái)判斷,當(dāng)前返回的是queueB,這里還是會(huì)去執(zhí)行dipatch_sync(queueA, block); 造成死鎖
因?yàn)殛?duì)列有層級(jí)關(guān)系,所以 “檢查當(dāng)前隊(duì)列是否為執(zhí)行同步派發(fā)所用的隊(duì)列” 這種辦法,并不是總是奏效的。
要解決這個(gè)問(wèn)題,可以通過(guò)GCD 所提供的功能來(lái)設(shè)定 “隊(duì)列特有數(shù)據(jù)”,此功能可以把任意數(shù)據(jù)以鍵值對(duì)的形式關(guān)聯(lián)到隊(duì)列里。假如根據(jù)指定的鍵獲取不到關(guān)聯(lián)數(shù)據(jù),那么就會(huì)沿著層級(jí)體系向上查找,直到找到數(shù)據(jù)或到根隊(duì)列為止。
dispatch_queue_t queueA = dispatch_queue_creat("com.pengxuyuan.queueA",NULL);
dispatch_queue_t queueB = dispatch_queue_creat("com.pengxuyuan.queueB",NULL);
static int kQueueSpecific;
CFStringRef queueSepcificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA,
&kQueueSpecific,
(void *)queueSepcificValue,
(dispatch_function_t));
dispatch_sync(queueB, ^{
dispatch_block_t block = ^();
CFStringRef retrievedValue = dispatch_queue_set_specific(&kQueueSpecific);
if(retrievedValue){
block();
}else{
dipatch_sync(queueA, block);
}
});
dispatch_get_current_queue 函數(shù)的行為常常與開發(fā)者所預(yù)期的不同。此函數(shù)已經(jīng)廢棄,只應(yīng)做調(diào)試之用。
由于派發(fā)隊(duì)列是按層級(jí)來(lái)組織的,所以無(wú)法單用某個(gè)隊(duì)列對(duì)象來(lái)描述 “當(dāng)前隊(duì)列” 這一概念。
dispatch_get_current_queue 函數(shù)用于解決由不可重入的代碼引發(fā)的死鎖,然而能用此函數(shù)的解決的問(wèn)題,通常也能改用 “隊(duì)列特定數(shù)據(jù)” 來(lái)解決。