iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇1/4
iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇2/4
iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇3/4
iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法 - 篇4/4
- 第六章 塊與大中樞派發(fā)
- 第37條:理解“塊”這一概念
- 第38條:為常用的塊類型創(chuàng)建typedef
- 第39條:用handler塊降低代碼分散程度
- 第40條:用塊引用其所屬對象時不要出現保留環(huán)
- 第41條:多用派發(fā)隊列,少用同步鎖
- 第42條:多用GCD,少用performSelector系列方法
- 第43條:掌握GCD及操作隊列的使用時機
- 第44條:通過Dispatch Group機制,根據系統(tǒng)資源狀況來執(zhí)行任務
- 第45條:使用DIspatch_once來執(zhí)行只需一次的線程安全代碼
- 第46條:不要使用dispatch_get_current_queue
- 第七章 系統(tǒng)框架
- 第47條:熟悉系統(tǒng)框架
- 第48條:多用塊枚舉,少用for循環(huán)
- 第49條:對自定義其內存管理語義的collection使用無縫橋接
- 第50條:構建緩存時選用NSCache而非NSDictionary
- 第51條:精簡initialize與load的實現代碼
- 第52條:別忘了NSTimer會保留其目標對象
第六章 塊與大中樞派發(fā)
當前多線程編程的核心就是“塊"(block)與“大中樞派發(fā)”(GrandCentral Dispatch,GCD)。這雖然是兩種不同的技術,但它們是一并引入的。
GCD是一種與塊有關的技術,它提供了對線程的抽象,而這種抽象則基于“派發(fā)隊列” (dispatch queue) 。
第37條:理解“塊”這一概念
塊的基礎知識
“塊”是一種可在C、C++及Objective-C代碼中使用的“詞法閉包"(lexical closure)。
- 塊是一種代替函數指針的語法結構,所以它是匿名的;
- 可將塊像對象一樣傳遞;
- 在定義“塊”的范圍內,它可以訪問到其中的全部變量;
塊可以實現閉包。塊用^符號(脫字符)來表示,后面跟著一對花括號,括號里面是塊的實現代碼。 例如,下面就是個簡單的塊
^ {
//Block implementation here
}
塊其實就是個值。與int、float或Objective-C對象一樣,也可以把塊賦給變量,然后像使用其他變量那樣使用它。塊類型的語法與函數指針近似。
塊類型的語法結構如下:
return_type (block_name)(parameters)
int (^addBlock) (int a, int b) = ^(int a, int b) {
return a + b;
};
定義好之后,就可以像函數那樣使用了。比方說,addBlock塊可以這樣用:
int add = addBlock (2 + 5) ; // add = 7
塊的強大之處是:在聲明它的范圍里,所有變量都可以為其所捕獲。這也就是說,那個范圍里的全部變量,在塊里依然可用。
默認情況下,為塊所捕獲的變量,是不可以在塊里修改的,編譯器會報錯。不過,聲明變量的時候可以加上__block修飾符,這樣就可以在塊內修改了。
如果塊所捕獲的變量是對象類型,那么就會自動保留它。系統(tǒng)在釋放這個塊的時候,也會將其一并釋放。這就引出了一個與塊有關的重要問題。塊本身可視為對象。實際上,在其他Objective-C對象所能響應的選擇子中,有很多是塊也可以響應的。而最重要之處則在于,塊本身也和其他對象一樣,有引用計數。當最后一個指向塊的引用移走之后,塊就回收了。 回收時也會釋放塊所捕獲的變量,以便平衡捕獲時所執(zhí)行的保留操作。
塊總能修改實例變量,所以在聲明時無須加__block。不過,如果通過讀取或寫入操作捕獲了實例變量,那么也會自動把self變量一并捕獲了,因為實例變量是與self所指代的實例關聯(lián)在一起的。直接訪問實例變量和通過self來訪問是等效的。
self->_anInstanceVariable = @"Something";
之所以要捕獲self變量,原因正在于此。然而,一定要記住:self也是個對象,因而塊在捕獲它時也會將其保留。如果self所指代 的那個對象同時也保留了塊,那么這種情況通常就會導致‘保留環(huán)’。
塊的內部結構
每個Objective-C對象都占據著某個內存區(qū)域。因為實例變量的個數及對象所包含的關聯(lián)數據互不相同,所以每個對象所占的內存區(qū)域也有大有小。塊本身也是對象,在存放塊對象的內存區(qū)域中,首個變量是指向Class對象的指針,該指針叫做isa(參見第14條)。其余內存里含有塊對象正常運轉所需的各種信息。

在內存布局中,最重要的就是invoke變量,這是個函數指針,指向塊的實現代碼。函數原型至少要接受一個void*型的參數,此參數代表塊。剛才說過,塊其實就是一種代替函數指針的語法結構,原來使用函數指針時,需要用“不透明的void指針”來傳遞狀態(tài)。而改用塊之后,則可以把原來用標準C語言特性所編寫的代碼封裝成簡明且易用的接口。
descriptor變量是指向結構體的指針,每個塊里都包含此結構體,其中聲明了塊對象的總體大小,還聲明了copy與dispose這兩個輔助函數所對應的函數指針。輔助函數在拷貝及丟棄塊對象時運行,其中會執(zhí)行一些操作,比方說,前者要保留捕獲的對象,而后者則將之釋放。
塊還會把它所捕獲的所有變量都拷貝一份。捕獲了多少個變量,就要占據多少內存空間。請注意,拷貝的并不是對象本身,而是指向這些對象的指針變量。invoke函數為何需要把塊對象作為參數傳進來呢?原因就在于,執(zhí)行塊時,要從內存中把這些捕獲到的變量讀出來。
全局塊、棧塊及堆塊
定義塊的時候,其所占的內存區(qū)域是分配在棧中的。這就是說,塊只在定義它的那個范圍內有效。為解決此問題,可給塊對象發(fā)送copy消息以拷貝之。這樣的話,就可以把塊從棧復制到堆了??截惡蟮膲K,可以在定義它的那個范圍之外使用。而且,一旦復制到堆上,塊就成了帶引用計數的對象了。后續(xù)的復制操作都不會真的執(zhí)行復制,只是遞增塊對象的引用計數。如果不再使用這個塊,那就應將其釋放,在ARC環(huán)境下會自動釋放,而手動管理引用計數時則需要自己來調用release方法。當引用計數降為0后,“分配在堆上的塊”(heap block)會像其他對象一樣,為系統(tǒng)所回收。而“分配在棧上的塊”(stack block)則無須釋放,因為棧內存本來就會自動回收。
除了“桟塊”和“堆塊”之外,還有一類塊叫做“全局塊”(global block)。這種塊不會捕捉任何狀態(tài)(比如外圍的變量等),運行時也無須有狀態(tài)來參與。塊所使用的整個內存區(qū)域,在編譯期已經完全確定了,因此,全局塊可以聲明在全局內存里,而不需要在每次用到的時候于棧中創(chuàng)建。另外,全局塊的拷貝操作是個空操作,因為全局塊決不可能為系統(tǒng)所回收。這種塊實際上相當于單例。
由于運行該塊所需的全部信息都能在編譯期確定,所以可把它做成全局塊。
要點:
- 塊是C、C++、Objective-C中的詞法閉包。
- 塊可接受參數,也可返回值。
- 塊可以分配在?;蚨焉?,也可以是全局的。分配在棧上的塊可拷貝到堆里,這樣的話,就和標準的Objective-C對象一樣,具備引用計數了。
第38條:為常用的塊類型創(chuàng)建typedef
每個塊都具備其“固有類型”(inherenttype),因而可將其賦給適當類型的變量。如果想把塊賦給變量,則需注意其類型。變量類型及相關賦值語句如下:
int (^variableName) (BOOL flag, int value) = ^(BOOL flag, int value) {
//Implementation return somelnt;
}
這個類型似乎和普通的類型大不相同,然而如果習慣函數指針的話,那么看上去就會覺得眼熟了。
為了隱藏復雜的塊類型,需要用到C語言中名為“類型定義”(type definition)的特性。 typedef關鍵字用于給類型起個易讀的別名。比如:
typedef int(^EOCSomeBlock)(BOOL flag, int value);
上面這條語句向系統(tǒng)中新增了一個名為EOCSomeBlock的類型。此后,不用再以復雜的塊類型來創(chuàng)建變量了,直接使用新類型即可:
EOCSomeBlock block = ^(BOOL flag, int value){
// Implementation
};
若不定義別名,則方法簽名會像下面這樣:
- (void)startWithCompletionHandler:(void(^)(NSData *data, NSError *error))completion;
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;
要點:
- 以typedef重新定義塊類型,可令塊變量用起來更加簡單。
- 定義新類型時應遵從現有的命名習慣,勿使其名稱與別的類型相沖突。
- 不妨為同一個塊簽名定義多個類型別名。如果要重構的代碼使用了塊類型的某個別名,那么只需修改相應typedef中的塊簽名即可,無須改動其他typedef。
第39條:用handler塊降低代碼分散程度
要點:
- 在創(chuàng)建對象時,可以使用內聯(lián)的handler塊將相關業(yè)務邏輯一并聲明。
- 在有多個實例需要監(jiān)控時,如果采用委托模式,那么經常需要根據傳入的對象來切換,而若改用handler塊來實現,則可直接將塊與相關對象放在一起。
- 設計API時如果用到了handler塊,那么可以增加一個參數,使調用者可通過此參數來決定應該把塊安排在哪個隊列上執(zhí)行。
第40條:用塊引用其所屬對象時不要出現保留環(huán)
使用塊來編程時,很容易導致“保留環(huán)”(retain cycle)。要想清楚塊可能會捕獲并保留哪些對象。如果這些對象又直接或間接保留了塊,那么就要考慮怎樣在適當的時機解除保留環(huán)。
要點:
- 如果塊所捕獲的對象直接或間接地保留了塊本身,那么就得當心保留環(huán)問題。
- 一定要找個適當的時機解除保留環(huán),而不能把責任推給API的調用者。
第41條:多用派發(fā)隊列,少用同步鎖
在Objective-C中,如果有多個線程要執(zhí)行同一份代碼,那么有時可能會出問題。這種情況下,通常要使用鎖來實現某種同步機制。在GCD出現之前,有兩種辦法,第一種是采用內置的“同步塊”(synchronization block):
- (void)synchronizedMethod {
@synchronized(self) {
//Safe
}
}
這種寫法會根據給定的對象,自動創(chuàng)建一個鎖,并等待塊中的代碼執(zhí)行完畢。執(zhí)行到這段代碼結尾處,鎖就釋放了。在本例中,同步行為所針對的對象是self。這么寫通常沒錯, 因為它可以保證每個對象實例都能不受干擾地運行synchronizedMethod方法。然而,濫用@synchronized(self)則會降低代碼效率,因為共用同一個鎖的那些同步塊,都必須按順序執(zhí)行。若是在self對象上頻繁加鎖,那么程序可能要等另一段與此無關的代碼執(zhí)行完畢,才能繼續(xù)執(zhí)行當前代碼,這樣做其實并沒有必要。
另一個辦法是直接使用NSLock對象:
_lock = [[NSLock alloc] init];
- (void)synchronizedMethod {
[_lock lock];
//Safe
[_lock unlock];
}
也可以使用NSRecursiveLock這種“遞歸鎖’(recursive lock) ,線程能夠多次持有該鎖,而不會出現死鎖(dead lock)現象。
這兩種方法都很好,不過也有其缺陷。比方說,在極端情況下,同步塊會導致死鎖,另外,其效率也不見得很高,而如果直接使用鎖對象的話,一旦遇到死鎖,就會非常麻煩。
-(NSString*)someString {
@synchronized(self) {
return _someString;
}
}
- (void)setSomeString:(NSString*)someString {
@synchronized(self) {
_someString = someString;
}
}
剛才說過,濫用@synchronized(self)會很危險,因為所有同步塊都會彼此搶奪同一個鎖。要是有很多個屬性都這么寫的話,那么每個屬性的同步塊都要等其他所有同步塊執(zhí)行完畢才能執(zhí)行,這也許并不是開發(fā)者想要的效果。我們只是想令每個屬性各自獨立地同步。
使用GCD柵欄(barrier),下列函數可以向隊列中派發(fā)塊,將其作為柵欄使用:
void dispatch_barrier_async(dispatch_queue_t queue,dispatch_block_t block);
void dispatch_barrier_sync(dispatch_queue_t queue,dispatch_block_t block);
在隊列中,柵欄塊必須單獨執(zhí)行,不能與其他塊并行。這只對并發(fā)隊列有意義,因為串行隊列中的塊總是按順序逐個來執(zhí)行的。并發(fā)隊列如果發(fā)現接下來要處理的塊是個柵欄塊 (barrier block) ,那么就一直要等當前所有并發(fā)塊都執(zhí)行完畢,才會單獨執(zhí)行這個柵欄塊。待柵欄塊執(zhí)行過后,再按正常方式繼續(xù)向下處理。
可以用柵欄塊來實現屬性的設置方法。在設置方法中使用了柵欄塊之后,對屬性的讀取操作依然可以并發(fā)執(zhí)行,但是寫入操作卻必須單獨執(zhí)行了。
_syncQueue = dispatch_qet_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueuef ^{
localSomeString = _someString;
});
return localSomeString;
};
- (void)setSomeString: (NSString*)someString {
dispatch_barrier_async(_syncQueue,^{
_someString = someString;
});
}

在這個并發(fā)隊列中,讀取操作是用普通的塊來實現的,而寫入操作則是用柵欄塊來實現的。 讀取操作可以并行,但寫入操作必須單獨執(zhí)行,因為它是柵欄塊。
這種做法肯定比使用串行隊列要快。注意,設置函數也可以改用同步的柵欄塊(synchronous barrier)來實現,那樣做可能會更髙效。
要點:
- 派發(fā)隊列可用來表述同步語義(synchronization semantic),這種做法要比使用
@synchronized塊或NSLock對象更簡單。- 將同步與異步派發(fā)結合起來,可以實現與普通加鎖機制一樣的同步行為,而這么做卻不會阻塞執(zhí)行異步派發(fā)的線程。
- 使用同步隊列及柵欄塊,可以令同步行為更加髙效。
第42條:多用GCD,少用performSelector系列方法
NSObject定義了幾個方法, 令開發(fā)者可以隨意調用任何方法。
[object performSelector: selector];
這種方式看上去似乎多余。如果某個方法只是這么來調用的話,那么此方式確實多余。 然而,如果選擇子是在運行期決定的,那么就能體現出此方式的強大之處了。這就等于在動態(tài)綁定之上再次使用動態(tài)綁定。
performSelector系列方法還有個功能,就是可以延后執(zhí)行選擇子,或將其放在另一個線程上執(zhí)行。下面列出了此方法中一些更為常用的版本:
- (void)performSelector:(SEL)selector withObject:(id)argument afterDelay:(NSTimelnterval)delay
- (void)performSelector:(SEL)selector onThread:(NSThread *)thread withObject:(id)argument waitUntilDone:(BOOL)wait
- (void)performSelectorOnMainThread:(SEL)selector withObject:(id)argument waitUntilDone:(BOOL)wait
要點:
- performSelector系列方法在內存管理方面容易有疏失。它無法確定將要執(zhí)行的選擇子具體是什么,因而ARC編譯器也就無法插入適當的內存管理方法。
- performSelector系列方法所能處理的選擇子太過局限了,選擇子的返回值類型及發(fā)送 給方法的參數個數都受到限制。
- 如果想把任務放在另一個線程上執(zhí)行,那么最好不要用performSelector系列方法,而是應該把任務封裝到塊里,然后調用大中樞派發(fā)機制的相關方法來實現。
第43條:掌握GCD及NSOperationQueue(操作隊列)的使用時機
在執(zhí)行后臺任務時,GCD并不一定是最佳方式。還有一種技術叫做NSOperationQueue,“操作隊列” (operation queue)在GCD之前就有了,其中某些設計原理因操作隊列而流行,GCD就是基于這些原理構建的。實際上,從iOS4與Mac OSX 10.6開始,操作隊列在底層是用GCD來實現的。
在兩者的諸多差別中,首先要注意:GCD是純C的API,而操作隊列則是Objective-C 的對象。在GCD中,任務用塊來表示,而塊是個輕量級數據結構(參見第37條)。與之相 反,“操作"(operation)則是個更為重量級的Objective-C對象。雖說如此,但GCD并不總是最佳方案。有時候采用對象所帶來的開銷微乎其微,使用完整對象所帶來的好處反而大大超過其缺點。
使用NSOperation及NSOperationQueue的好處如下:
- 取消某個操作。運行任務之前, 可以在NSOperation對象上調用cancel方法,該方法會設置對象內的標志位,用以表明此任務不需執(zhí)行,不過,已經啟動的任務無法取消。若是不使用操作隊列,而是把塊安排到GCD隊列,那就無法取消了。那套架構是“安排好任務之后就不管了"(fire and forget)。
- 指定操作間的依賴關系。使特定的操作必須在另外一個操作順利執(zhí)行完畢后方可執(zhí)行。
- 通過鍵值觀測機制監(jiān)控NSOperation對象的屬性。NSOperation對象有許多屬性都適合通過鍵值觀測機制(簡稱KVO)來監(jiān)聽,比如可以通過isCancelled屬性來判斷任務是否已取消,又比如可以通過isFinished屬性來判斷任務是否已完成。如果想在某個任務變更其狀態(tài)時得到通知,或是想用比GCD更為精細的方式來控制所要執(zhí)行的任務,那么鍵值觀測機制會很有用。
- 指定操作的優(yōu)先級。操作的優(yōu)先級表示此操作與隊列中其他操作之間的優(yōu)先關系。優(yōu)先級高的操作先執(zhí)行,優(yōu)先級低的后執(zhí)行。操作隊列的調度算法(scheduling algorithm)雖“不透明"(opaque),但必然是經過一番深思熟慮才寫成的。反之,GCD 則沒有直接實現此功能的辦法。GCD的隊列確實有優(yōu)先級,不過那是針對整個隊列來說的,而不是針對每個塊來說的。
- 重用 NSOperation 對象。系統(tǒng)內置了一些 NSOperation 的子類(比如 NSBlockOperation) 供開發(fā)者調用,要是不想用這些固有子類的話,那就得自己來創(chuàng)建了。這些類就是普通的Objective-C對象,能夠存放任何信息。對象在執(zhí)行時可以充分利用存于其中的信息,而且還可以隨意調用定義在類中的方法。這就比派發(fā)隊列中那些簡單的塊要強大許多。這些NSOperation類可以在代碼中多次使用,它們符合軟件開發(fā)中的“不重復” (Don ’ t Repeat Yourself, DRY )原則。
有一個API選用了操作隊列而非派發(fā)隊列,這就是NSNotificationCemer,開發(fā)者可通過其中的方法來注冊監(jiān)聽器,以便在發(fā)生相關事件時得到通知,而這個方法接受的參數是塊, 不是選擇子。方法原型如下:
- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock: (void(^)(NSNotification *))block
本來這個方法也可以不使用操作隊列,而是把處理通知事件所用的塊安排在派發(fā)隊列里。但實際上并沒有這樣做,其設計者顯然使用了高層的Objective-C API。在這種情況下, 兩套方案的運行效率沒多大差距。設計這個方法的人可能不想使用派發(fā)隊列,因為那樣做將依賴于GCD,而這種依賴沒有必要,前面說過,塊本身和GCD無關,所以如果僅使用塊的話,就不會引入對GCD的依賴了。也有可能是編寫這個方法的人想全部用Objective-C來描述,而不想使用純C的東西。
經常會有人說:應該盡可能選用高層API,只在確有必要時才求助于底層。筆者也同意這個說法,但我并不盲從。某些功能確實可以用高層的Objective-C方法來做,但這并不等于說它就一定比底層實現方案好。要想確定哪種方案更佳,最好還是測試一下性能。
要點:
- 在解決多線程與任務管理問題時,派發(fā)隊列并非唯一方案。
- 操作隊列提供了一套高層的Objective-C API,能實現純GCD所具備的絕大部分功能, 而且還能完成一些更為復雜的操作,那些操作若改用GCD來實現,則需另外編寫代碼。
第44條:通過Dispatch Group機制,根據系統(tǒng)資源狀況來執(zhí)行任務
dispatch group 是GCD的一項特性,能夠把任務分組。調用者可以等待這組任務執(zhí)行完畢,也可以在提供回調函數之后繼續(xù)往下執(zhí)行,這組任務完成時,調用者會得到通知。這個功能有許多用途,其中最重要、最值得注意的用法,就是把將要并發(fā)執(zhí)行的多個任務合為一組,于是調用者就可以知道這些任務何時才能全部執(zhí)行完畢。
創(chuàng)建dispatch group:
dispatch_group_dispatch_group_create();
想把任務編組,有兩種辦法:
第一種:
void dispatch_group_async(dispatch_group_t group,dispatch_queue_t queue, dispatch_block_t block);
第二種:
void dispatch_group_enter(dispatch_group_t group); //使分組里正要執(zhí)行的任務數遞增
void dispatch_group_leave(dispatch_group_t group);//使之遞減
調用了 dispatch_group_enter以后,必須有與之對應的dispatch group leave才行。這與引用計數(參見第29條)相似。在使用dispatch group時,如果調用enter之后,沒有相應的leave操作,那么這一組任務就永遠執(zhí)行不完。
下面這個函數可用于等待dispatch group執(zhí)行完畢:
long dispatch_group_wait(dispatch_group_t group,dispatch_time_t timeout);
timeout參數表示函數在等待dispatch group執(zhí)行完畢時,應該阻塞多久。如果執(zhí)行dispatch group所需的時間小于timeout,則返回0,則返回非0值。此參數也可以取常量DISPATCH_ TIME_FOREVER,這表示函數會一直等著dispatch group執(zhí)行完,而不會超時(time out)。
若當前線程不應阻塞,則可用notify函數來取代wait:
void dispatch_group_notify(dispatch_group_t group,dispatch_queue_t queue, dispatch_block_t block);
模擬多線程耗時操作(比如異步請求網絡):
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_group_async(group, globalQueue, ^{
sleep(3);
NSLog(@"thread:%@ block1結束",[NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_group_async(group, globalQueue, ^{
sleep(3);
NSLog(@""thread:%@ block2結束",[NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
//全部結束
});
開發(fā)者未必總需要使用dispatch group。有時候采用單個隊列搭配標準的異步派發(fā),也可實現同樣效果。
遍歷某個collection,并在其每個元素上執(zhí)行任務,dispatcl_apply會持續(xù)阻塞,直到所有任務都執(zhí)行完畢為止。
void dispatch_apply(size_t iterations,dispatch_queue_t queue, void(^block)(size_t));
要點:
- 一系列任務可歸入一個dispatch group之中。開發(fā)者可以在這組任務執(zhí)行完畢時獲得通知。
- 通過dispatch group,可以在并發(fā)式派發(fā)隊列里同時執(zhí)行多項任務。此時GCD會根據系統(tǒng)資源狀況來調度這些并發(fā)執(zhí)行的任務。開發(fā)者若自己來實現此功能,則需編寫大量代碼。
第45條:使用dispatch_once來執(zhí)行只需運行一次的線程安全代碼
void dispatch_once (dispatch_once_t *token,dispatch_block_t block);
此函數接受類型為dispatch_once_t的特殊參數,筆者稱其為“標記”(token),此外還接受塊參數。對于給定的標記來說,該函數保證相關的塊必定會執(zhí)行,且僅執(zhí)行一次。首次調用該函數時,必然會執(zhí)行塊中的代碼,最重要的一點在于,此操作完全是線程安全的。請注意,對于只需執(zhí)行一次的塊來說,每次調用函數時傳入的標記都必須完全相同。因此,開發(fā)者通常將標記變量聲明在static或global作用域里,可以保證編譯器在每次執(zhí)行sharedlnstance方法時都會復用這個變量,而不會創(chuàng)建新變量。
+ (id)sharedlnstance {
static EOCClass *sharedlnstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedlnstance = [[self alloc] init];
});
return sharedlnstance;
}
此外,dispatch_once更高效。它沒有使用重量級的同步機制,若是那樣做的話,每次運行代碼前都要獲取鎖,相反,此函數采用“原子訪問”(atomic access)來査詢標記,以判斷其所對應的代碼原來是否已經執(zhí)行過。筆者在自己裝有64位Mac OS X 10.8.2系統(tǒng)的電腦上簡單測試了性能,分別采用@synchronized方式及dispatch once方式來實現sharedlnstance方法,結果顯示,后者的速度幾乎是前者的兩倍。
要點:
- 經常需要編寫“只需執(zhí)行一次的線程安全代碼”(thread-safe single-code execution)。通過GCD所提供的dispatch_once函數,很容易就能實現此功能。
- 標記應該聲明在static或global作用域中,這樣的話,在把只需執(zhí)行一次的塊傳給dispatch once函數時,傳進去的標記也是相同的。
第46 條:不要使用dispatch_get_current_queue (待補充)
使用GCD時,經常需要判斷當前代碼正在哪個隊列上執(zhí)行,向多個隊列派發(fā)任務時, 更是如此。文檔中說,此函數返回當前正在執(zhí)行代碼的隊列,該函數有種典型的錯誤用法(antipattem,“反模式”)。
要點:
- dispatch_get_current_queue函數的行為常常與開發(fā)者所預期的不同。此函數已經廢棄, 只應做調試之用。
- 由于派發(fā)隊列是按層級來組織的,所以無法單用某個隊列對象來描述“當前隊列”這一概念。
- dispatch_get_current_queue函數用于解決由不可重入的代碼所引發(fā)的死鎖,然而能用此函數解決的問題,通常也能改用“隊列特定數據”來解決。
第七章 系統(tǒng)框架
雖說不使用系統(tǒng)框架也能編寫Objective-C代碼,但幾乎沒人這么做。即便是NSObject這個標準的根類,也屬于Foundation框架,而非語言本身。若不使用Foundation,就必須自己編寫根類。
第47條:熟悉系統(tǒng)框架
將一系列代碼封裝為動態(tài)庫(dynamic library),并在其中放入描述其接口的頭文件,這樣做出來的東西就叫框架。有時為iOS平臺構建的第三方框架所使用的是靜態(tài)庫(static library),這是因為iOS應用程序不允許在其中包含動態(tài)庫。這些東西嚴格來講并不是真正的框架,然而也經常視為框架。不過,所有iOS平臺的系統(tǒng)框架仍然使用動態(tài)庫。
在為Mac OS X或iOS系統(tǒng)開發(fā)“帶圖形界面的應用程序"(graphical application)時,會用到名為Cocoa的框架,在iOS上稱為Cocoa Touch。其實Cocoa本身并不是框架,但是里面集成了一批創(chuàng)建應用程序時經常會用到的框架。
開發(fā)者會碰到的主要框架就是Foundation,像是NSObject、NSArray、NSDictionary等類都在其中。Foundation框架中的類,使用NS這個前綴,此前綴是在Objective-C語言用作NextStep操作系統(tǒng)的編程語言時首度確定的。Foundation框架真可謂所有Objective-C應用程序的“基礎”,若是沒有它,那么本書大部分內容就不知所云了。
還有個與Foundation相伴的框架,叫做Core Foundation。雖然從技術上講,Core Foundation框架不是Objective-C框架,但它卻是編寫Objectve-C應用程序時所應熟悉的重要框架, Foundation框架中的許多功能,都可以在此框架中找到對應的C語言API。Core Foundation 與Foundation不僅名字相似,而且還有更為緊密的聯(lián)系。有個功能叫做“無縫橋接”(toll- free bridging ), 可以把Core Foundation中的C語言數據結構平滑轉換為Foundation中的Objective-C對象,也可以反向轉換。比方說,Foundation框架中的字符串是NSString,而它可以轉換為Core Foundation里與之等效的CFString對象。無縫橋接技術是用某些相當復雜的代碼實現出來的,這些代碼可以使運行期系統(tǒng)把Core Foundation框架中的對象視為普通的Objective-C對象。但是,像無縫橋接這么復雜的技術,想自己編寫代碼實現它,可不太容易。開發(fā)程序時可以使用此功能。
除了 Foundation與Core Foundation之外,還有很多系統(tǒng)庫,其中包括但不限于下面列出的這些:
-
CFNetwork此框架提供了C語言級別的網絡通信能力 -
CoreAudio該框架所提供的C語言API可用來操作設備上的音頻硬件。 -
AVFoundation此框架所提供的Objective-C對象可用來回放并錄制音頻及視頻 -
CoreData此框架所提供的Objective-C接口可將對象放入數據庫 -
CoreText此框架提供的C語言接口可以高效執(zhí)行文字排版及渲染操作。
可以看出Objective-C編程的一項重要特點,那就是:經常需要使用底層的C語言級API。用C語言來實現API的好處是,可以繞過Objective-C的運行期系統(tǒng),從而提升執(zhí)行速度。當然,由于ARC只負責 Objective-C的對象(參見第30條),所以使用這些API時尤其需要注意內存管理問題。若想使用這種框架,一定得熟悉C語言基礎才行。
讀者可能會編寫使用UI框架的Mac OS X或iOS應用程序。這兩個平臺的核心UI框架分別叫做AppKit及UIKit,它們都提供了構建在Foundation與Core Foundation之上的Objective-C類。在這些主要的UI框架之下,是Core Animation與Core Graphics框架。
Core Animation是用Objective-C語言寫成的,它提供了一些工具,而UI框架則用這些工具來渲染圖形并播放動畫。Core Animation本身并不是框架,它是QuartzCore框架的一部分。
Core Graphics框架以C語言寫成,其中提供了2D渲染所必備的數據結構與函數,例如, 其中定義了CGPoint、CGSize、CGRect等數據結構,比如UlKit框架中的UlView類在確定視圖控件之間的相對位置時,這些數據結構都要用到。
要點:
- 許多系統(tǒng)框架都可以直接使用。其中最重要的是Foundation與Core Foundation,這兩個框架提供了構建應用程序所需的許多核心功能。
- 很多常見任務都能用框架來做,例如音頻與視頻處理、網絡通信、數據管理等。
- 請記?。河眉僀寫成的框架與用Objective-C寫成的一樣重要,若想成為優(yōu)秀的Objective-C開發(fā)者,應該掌握C語言的核心概念。
第48條:多用塊枚舉,少用for循環(huán)
// 數組
NSArray *anArray = /* ??? */;
[anArray enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop){
//Do something with object
if (shouldStop) {
*stop = YES;
}
}];
// Dictionary
NSDictionary *aDictionary = /* ??? */;
[aDictionary enumerateObjectsUsingBlock:^(id key, id object, BOOL *stop){
//Do something
if (shouldStop) {
*stop = YES;
}
}];
//Set
NSSet *anSet = /* ??? */;
[anSet enumerateObjectsUsingBlock:^(id object, BOOL *stop){
//Do something with object
if (shouldStop) {
*stop = YES;
}
}];
優(yōu)點:
- 遍歷時可以直接從塊里獲取更多信息;
- 可以通過設定stop變量值來終止循環(huán),當然,使用其他幾種遍歷方式時,也可以通過break來終止循環(huán);
- 能夠修改塊的方法簽名,以免進行類型轉換操作,如果能夠確知某collection里的對象是什么類型, 那就應該使用這種方法指明其類型。
反向遍歷
向其傳入 NSEnumerationReverse“選項掩碼”(option mask):
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^)(id obj, NSUInteger idx, BOOL *stop))block
- (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^) (id key, id obj, BOOL *stop) ) block
NSEnumerationOptions類型是個enum,其各種取值可用“按位或”(bitwise OR)連接,用以表明遍歷方式。例如,開發(fā)者可以請求以并發(fā)方式執(zhí)行各輪迭代。如果使用此選項,那么底層會通過GCD來處理并發(fā)執(zhí)行事宜,具體實現時很可能會用到dispatch group (參見第44條)。不過,到底如何來實現,不是本條所要討論的內容。
要注意:只有在遍歷數組或有序set等有順序的collection時,這么做才有意義。
要點:
- 遍歷collection有四種方式。最基本的辦法是for循環(huán),其次是NSEnumerator遍歷法 及快速遍歷法,最新、最先進的方式則是“塊枚舉法”。
- “塊枚舉法”本身就能通過GCD來并發(fā)執(zhí)行遍歷操作,無須另行編寫代碼。而采用其 他遍歷方式則無法輕易實現這一點。
- 若提前知道待遍歷的collection含有何種對象,則應修改塊簽名,指出對象的具體類型。
第49條:對自定義其內存管理語義的collection使用無縫橋接
使用“無縫橋接”技術,可以在定義于Foundation框架中的Objective-C類和定義于 Core Foundation框架中的C數據結構之間互相轉換。筆者將C語言級別的API稱為數據結構,而沒有稱其為類或對象,這是因為它們與Objective-C中的類或對象并不相同。
NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li",, CFArrayGetCount (aCFArray));
//Output: Size of array = 5
轉換操作中的__bridge告訴ARC (參見第30條)如何處理轉換所涉及的Objective-C對象。__bridge本身的意思是:ARC仍然具備這個Objective-C對象的所有權。而_bridge_retained則與之相反,意味著ARC將交出對象的所有權。若是前面那段代碼改用它來實現, 那么用完數組之后就要加上CFRelease(aCFArray)以釋放其內存。與之相似,反向轉換可通過_bridge_transfer來實現。比方說,想把CFArrayRef轉換為NSArray *,并且想令ARC獲得對象所有權,那么就可以采用此種轉換方式。這三種轉換方式稱為“橋式轉換"(bridged cast)。
在使用Foundation框架中的字典對象時會遇到一個大問題,那就是其鍵的內存管理語義為“拷貝”,而值的語義卻是“保留”。除非使用強大的無縫橋接技術,否則無法改變其語義。
要點:
- 通過無縫橋接技術,可以在Foundation框架中的Objective-C對象與Core Foundation框架中的C語言數據結構之間來回轉換。
- 在Core Foundation層面創(chuàng)建collection時,可以指定許多回調函數,這些函數表示此collection應如何處理其元素。然后,可運用無縫橋接技術,將其轉換成具備特殊內存管理語義的Objective-C collection。
第50條:構建緩存時選用NSCache而非NSDictionary
NSCache類是Foundation框架專為處理內存緩存而設計的。
NSCache勝過NSDictionary之處在于以下幾個方面:
- 當系統(tǒng)資源將要耗盡時,NSCache可以自動刪減緩存,還會先行刪減“最久未使用的"(lease recently used)對象。 如果采用普通的字典,那么就要自己編寫掛鉤,在系統(tǒng)發(fā)出“低內存”(low memory)通知時手工刪減緩存,十分復雜。
- NSCache并不會“拷貝”鍵,而是會“保留”它。NSCache對象不拷貝鍵的原因在于:很多時候,鍵都是由不支持拷貝操作的對象來充當的。因此,NSCache不會自動拷貝鍵,所以說, 在鍵不支持拷貝操作的情況下,該類用起來比字典更方便。
- NSCache是線程安全的。 而NSDictionary則絕對不具備此優(yōu)勢,意思就是:在開發(fā)者自己不編寫加鎖代碼的前提下,多個線程便可以同時訪問NSCache。對緩存來說,線程安全通常很重要,因為開發(fā)者可能要在某個線程中讀取數據,此時如果發(fā)現緩存里找不到指定的鍵,那么就要下載該鍵所對應的數據了。而下載完數據之后所要執(zhí)行的回調函數,有可能會放在背景線程中運行,這樣的話,就等于是用另外一個線程來寫入緩存了。
- 開發(fā)者可以操控緩存刪減其內容的時機。有兩個與系統(tǒng)資源相關的尺度可供調整,其一是緩存中的對象總數,其二是所有對象的“總開銷"(overall cost)。開發(fā)者在將對象加入緩存時,可為其指定“開銷值”。當對象總數或總開銷超過上限時,緩存就可能會刪減其中的對象了,在可用的系統(tǒng)資源趨于緊張時,也會這么做。然而要注意,“可能”會刪減某個對象, 并不意味著“一定”會刪減這個對象。刪減對象時所遵照的順序,由具體實現來定。這尤其說明:想通過調整“開銷值”來迫使緩存優(yōu)先刪減某對象,不是個好主意。
注意:向緩存中添加對象時,只有在能很快計算出“開銷值”的情況下,才應該考慮采用這個尺度。若計算過程很復雜,那么照這種方式來使用緩存就達不到最佳效果了,而緩存的本意則是要增加應用程序響應用戶操作的速度。
如何使用:比如下載數據所用的URL,就是緩存的鍵。若緩存未命中(cache miss) ,則下載數據并將其放入緩存。而數據的“開銷值”則設為其長度。創(chuàng)建NSCache時,將其中可緩存的總對象數目上限設為100,將“總開銷’上限設為5MB,不過,由于‘開銷值’以‘字節(jié)’ 為單位,所以要通過算式將MB換箅成字節(jié)。
還有個類叫做NSPurgeableData,和NSCache搭配起來用,效果很好,此類是NSMutiableData的子類,而且實現了NSDiscardableContent協(xié)議。如果某個對象所占的內存能夠根據需要隨時丟棄,那么就可以實現該協(xié)議所定義的接口。這就是說,當系統(tǒng)資源緊張時,可以把保存NSPurgeableData對象的那塊內存釋放掉。NSDiscardableContent協(xié)議里定義了名為isContentDiscarded的方法,可用來査詢相關內存是否已釋放。
要點:
- 實現緩存時應選用NSCache而非NSDictionary對象。因為NSCache可以提供優(yōu)雅的自動刪減功能,而且是“線程安全的”,此外,它與字典不同,并不會拷貝鍵。
- 可以給NSCache對象設置上限,用以限制緩存中的對象總個數及“總成本”,而這些尺度則定義了緩存刪減其中對象的時機。但是絕對不要把這些尺度當成可靠的“硬限制”(hard limit),它們僅對NSCache起指導作用。
- 將
NSPurgeableData與NSCache搭配使用,可實現自動清除數據的功能,也就是說,當NSPurgeableData對象所占內存為系統(tǒng)所丟棄時,該對象自身也會從緩存中移除。- 如果緩存使用得當,那么應用程序的響應速度就能提高。只有那種“重新計算起來很費事的”數據,才值得放入緩存,比如那些需要從網絡獲取或從磁盤讀取的數據。
第51條:精簡initialize與load的實現代碼
在Objective-C中,絕大多數類都繼承自NSObject這個根類,而該類有兩個方法,可用來實現這種初始化操作:
+ (void)load
+ (void)initialize
首先要講的是load方法:
對于加入運行期系統(tǒng)中的每個類(class)及分類(category)來說,必定會調用此方法,而且僅調用一次。當包含類或分類的程序庫載入系統(tǒng)時,就會執(zhí)行此方法,而這通常就是指應用程序啟動的時候,若程序是為iOS平臺設計的,則肯定會在此時執(zhí)行。Mac OS X應用程序更自由一些,它們可以使用“動態(tài)加載”(dynamic loading)之類的特性,等應用程序啟動好之后再去加載程序庫。如果分類和其所屬的類都定義了load方法,則先調用類里的,再調用分類里的。
load方法的問題在于,執(zhí)行該方法時,運行期系統(tǒng)處于“脆弱狀態(tài)"(fragilestate)。在執(zhí)行子類的load方法之前,必定會先執(zhí)行所有超類的load方法,而如果代碼還依賴了其他程序庫,那么程序庫里相關類的load方法也必定會先執(zhí)行。然而,根據某個給定的程序庫,卻無法判斷出其中各個類的載入順序。因此,在load方法中使用其他類是不安全的。
有個重要的事情需注意,那就是load方法并不像普通的方法那樣,它并不遵從那套繼承規(guī)則。如果某個類本身沒實現load方法,那么不管其各級超類是否實現此方法,系統(tǒng)都不會調用。此外,分類和其所屬的類里,都可能出現load方法。此時兩種實現代碼都會調用,類的實現要比分類的實現先執(zhí)行。
而且load方法務必實現得精簡一些,也就是要盡量減少其所執(zhí)行的操作,因為整個應用程序在執(zhí)行l(wèi)oad方法時都會阻塞。如果load方法中包含繁雜的代碼,那么應用程序在執(zhí)行期間就會變得無響應。 其真正用途僅在于調試程序,比如可以在分類里編寫此方法,用來判斷該分類是否已經正確載入系統(tǒng)中。
想執(zhí)行與類相關的初始化操作,還有個辦法,就是覆寫initialize方法:
對于每個類來說,該方法會在程序首次用該類之前調用,且只調用一次。它是由運行期系統(tǒng)來調用的,絕不應該通過代碼直接調用。
其雖與load相似,但卻有幾個非常重要的區(qū)別:
- 它是“惰性調用的”,也就是說,只有當程序用到了相關的類時,才會調用。 因此,如果某個類一直都沒有使用,那么其initialize方法就一直不會運行。這也就等于說,應用程序無須先把每個類的initialize都執(zhí)行一遍,這與load方法不同,對于load來說,應 用程序必須阻塞并等著所有類的load都執(zhí)行完,才能繼續(xù)。
- 此方法與load還有個區(qū)別,就是運行期系統(tǒng)在執(zhí)行該方法時,是處于正常狀態(tài)的,因此,從運行期系統(tǒng)完整度上來講,此時可以安全使用并調用任意類中的任意方法。而且,運行期系統(tǒng)也能確保initialize方法一定會在“線程安全的環(huán)境”(thread-safe environment)中執(zhí)行。
- initialize方法與其他消息一樣,如果某個類未實現它,而其超類實現了,那么就會運行超類的實現代碼。
#import <Foundation/Foundation.h>
@interface EOCBaseClass : NSObject
@end
@implementation EOCBaseClass
+ (void)initialize {
NSLog (@"%@ initialize", self);
}
@end
@interface EOCSubClass : EOCBaseClass
@end
@implementation EOCSubClass
@end
即便EOCSubClass類沒有實現initialize方法,它也會收到這條消息。由各級超類所實現的initialize也會先行調用。所以,首次使用EOCSubClass時,控制臺會輸出如下消息:
EOCBaseClass initialize
EOCSubClass initialize
與其他方法(除去load) —樣, initialize也遵循通常的繼承規(guī)則,所以,當初始化基類EOCBaseClass時,EOCBaseClass中定義的initialize方法要運行一遍,而當初始化子類EOCSubClass時,由于該類并未覆寫此方法,因而還要把父類的實現代碼再運行一遍。鑒于此,通常都會這么來實現initialize方法:
+ (void)initialize {
if(self == [EOCBaseClass class]) {
NSLog (@"%@ initialize", self);
}
}
加上這條檢測語句之后,只有當開發(fā)者所期望的那個類載入系統(tǒng)時,才會執(zhí)行相關的初 始化操作。如果把剛才的例子照此改寫,那就不會打印出兩條記錄信息了,這次只輸出一條:
EOCBaseClass initialize
若某個全局狀態(tài)無法在編譯期初始化,則可以放在initialize里來做:
//EOCClass.h
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject
@end
//EOCClass .m
#import "EOCClass.h"
static const int klnterval = 10;
static NSMutableArray *kSomeObjects;
@implementation EOCClass
+ (void)initialize {
if (self == [EOCClass class]) {
kSomeObjects = [NSMutableArray new];
}
}
整數可以在編譯期定義,然而可變數組不行,因為它是個Objective-C對象,所以創(chuàng)建實例之前必須先激活運行期系統(tǒng)。注意,某些Objective-C對象也可以在編譯期創(chuàng)建,例如NSString實例。然而,創(chuàng)建下面這種對象會令編譯器報錯:
static NSMutableArray *kSomeObjects = [NSMutableArray new];
編寫load或initialize方法時,把代碼實現得簡單一些,能節(jié)省很多調試時間。除了初始化全局狀態(tài)之外,如果還有其他事情要做,那么可以專門創(chuàng)建一個方法來執(zhí)行這些操作,并要求該類的使用者必須在使用本類之前調用此方法。比如說,如 果“單例類"(singleton class)在首次使用之前必須執(zhí)行一些操作,那就可以采用這個辦法。
要點:
- 在加載階段,如果類實現了load方法,那么系統(tǒng)就會調用它。分類里也可以定義此方法,類的load方法要比分類中的先調用。與其他方法不同,load方法不參與覆寫機制。
- 首次使用某個類之前,系統(tǒng)會向其發(fā)送initialize消息。由于此方法遵從普通的覆寫規(guī)則,所以通常應該在里面判斷當前要初始化的是哪個類。
- load與initialize方法都應該實現得精簡一些,這有助于保持應用程序的響應能力,也能減少引入“依賴環(huán)”(interdependency cycle) 的幾率。
- 無法在編譯期設定的全局常量,可以放在initialize方法里初始化。
第52條:別忘了NSTimer會保留其目標對象
計時器要和“運行循環(huán)"(runloop)相關聯(lián),運行循環(huán)到時候會觸發(fā)任務。
方法1:創(chuàng)建計時器,并將其預先安排在當前運行循環(huán)中:
+ (NSTimer *)scheduledTimerWithTimelnterval:(NSTimelnterval)seconds target:(id)target selector:(SEL)selector userlnfo:(id)userinfo repeats:(BOOL)repeats;
方法2:先創(chuàng)建timer,然后再將其加入到當前的runloop中,并指定mode:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
方法3:iOS10.0及之后的系統(tǒng)才可以使用的,解決了內存泄露的問題:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
無論采用哪種方式,只有把計時器放在運行循環(huán)里,它才能正常觸發(fā)任務。
target與selector參數表示計時器將在哪個對象上調用哪個方法。計時器會保留其目標對象,等到自身“失效”時再釋放此對象。調用invalidate方法可令計時器失效。
由于計時器會保留其目標對象,所以反復執(zhí)行任務通常會導致應用程序出問題。也就是說, 設置成重復執(zhí)行模式的那種計時器,很容易引入“保留環(huán)”。
這個問題可通過“塊”來解決。雖然計時器當前并不直接支持塊,但是可以用下面這段代碼為其添加此功能:
#import <Foundation/Foundation.h>
@interface NSTimer (EOCBlocksSupport)
+ (NSTimer*)eoc_scheduledTimerWithTimelnterval:(NSTimelnterval)interval block:(void(^)())block repeats:(BOOL)repeats;
@end
@implementation NSTimer (EOCBlocksSupport)
+ (NSTimer*)eoc_scheduledTimerWithTimelnterval:(NSTimelnterval)interval block:(void(^)())block repeats:(BOOL)repeats {
return [self scheduledTimerWithTimelnterval:interval target:self selector:@selector(eoc_blocklnvoke:) userlnfo:[block copy] repeats:repeats];
}
+ (void)eoc_blocklnvoke:(NSTimer *timer {
void (^block)() = timer.userlnfo;
if (block) {
block();
}
}
@end
這個辦法為何能解決“保留環(huán)”問題呢?這段代碼將計時器所應執(zhí)行的任務封裝成“塊”,在調用計時器函數時,把它作為userlnfo參數傳進去。該參數可用來存放“不透明值”(opaque value) ,只要計時器還有效,就會一直保留著它。傳入參數時要通過copy方法將block拷貝到“堆”上(參見第37條),否則等到稍后要執(zhí)行它的時候,該塊可能已經無效了。計時器現在的target是NSTimer類對象,這是個單例,因此計時器是否會保留它,其實都無所謂。此處依然有保留環(huán),然而因為類對象(classobject)無須回收,所以不用擔心。
還沒有圓滿的完成,還有兩個問題要解決:還是存在保留環(huán)+需要停止計時器:
- (void)startPolling {
weak EOCClass *weakSelf = self;
_pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{
EOCClass *strongSelf = weakSelf;
[strongSelf p_doPoll];
} repeats:YES];
};
- (void)dealloc {
[_pollTimer invalidate];
}
這段代碼采用了一種很有效的寫法,它先定義了一個弱引用,令其指向self,然后使塊捕獲這個引用,而不直接去捕獲普通的self變量。也就是說,self不會為計時器所保留。當塊開始執(zhí)行時,立刻生成strong引用,以保證實例在執(zhí)行期間持續(xù)存活。
采用這種寫法之后,如果外界指向EOCClass實例的最后一個引用將其釋放,則該實例就可為系統(tǒng)所回收了?;厥者^程中還會調用計時器的invalidate方法,這樣的話,計時器就不會再執(zhí)行任務了。此處使用weak引用還能令程序更加安全,因為有時開發(fā)者可能在編寫dealloc時忘了調用計時器的invalidate方法,從而導致計時器再次運行,若發(fā)生此類情況,則塊里的weakSelf會變成nil。
要點:
- NSTimer對象會保留其目標,直到計時器本身失效為止,調用invalidate方法可令計時器失效,另外,一次性的計時器在觸發(fā)完任務之后也會失效。
- 反復執(zhí)行任務的計時器(repeating timer),很容易引入保留環(huán),如果這種計時器的目標對象又保留了計時器本身,那肯定會導致保留環(huán)。這種環(huán)狀保留關系,可能是直接發(fā)生的,也可能是通過對象圖里的其他對象間接發(fā)生的。
- 可以擴充NSTimer的功能,用“塊”來打破保留環(huán)。不過,除非NSTimer將來在公共接口里提供此功能,否則必須創(chuàng)建分類,將相關實現代碼加入其中。