目錄
內存管理
理解引用計數
引用計數工作原理
自動釋放池
保留環
以ARC簡化引用計數
使用ARC時必須遵循的方法命名規則
變量的內存管理語義
ARC如何清理實例變量
在dealloc方法中只釋放引用并解除監聽
編寫“異常安全代碼”時留意內存管理問題
以弱引用避免保留環
以“自動釋放池塊”降低內存峰值
用“僵尸對象”調試內存管理問題
不要使用retainCount
塊與大中樞派發
理解“塊”這一概念
塊的基礎知識
塊的內部結構
全局塊、棧塊及堆塊
為常用的塊類型創建typedef
用handler塊降低代碼分散程度
用塊引用其所屬對象時不要出現保留環
多用派發隊列,少用同步鎖
?編輯
多用GCD,少用performSelector系列方法
掌握GCD及操作隊列的使用時機
通過Dispatch Group機制,根據系統資源狀況來執行任務
使用dispatch_once來執行只需運行一次的線程安全代碼
不要使用dispatch_get_current_queue
系統框架
熟悉系統框架
多用枚舉塊,少用for循環
對自定義其內存管理語義的collection使用無縫橋接
構建緩存時選用NSCache而非NSdictionary
精簡initiakize與load的實現代碼
別忘了NSTimer會保留其目標對象
內存管理
理解引用計數
OC使用引用計數來管理內存。
引用計數工作原理
NSObject協議聲明了下面三個方法來操作計數器,以遞增或遞減其值:
retain 遞增保留計數
release 遞減保留計數
autorelease 待清理“自動釋放池”,再遞減保留計數
對象被創建后保留計數的變化過程如下圖所示:
一般調用完release后都會清空指針,這就能保證不會出現可能指向無效對象的指針。
自動釋放池
有時可以不調用release,改為調用autorelease,此方法會在稍后遞減計數,通常是下一次“事件循環”時遞減。
使用自動釋放池autorelease可以延長對象生命期,使其在跨越方法調用邊界后依然可以存活一段時間。
保留環
引用計數經常要注意的一個問題是“保留環”,“保留環”會導致內存泄漏,因為循環中的對象保留計數不會降為0,通常采用“若引用”來解決這一問題。
以ARC簡化引用計數
ARC,即自動引用計數,所做的事情就是自動管理引用計數。由于ARC自動執行retain、release、autorelease等操作,所以不能調用上述方法。
ARC在調用這些方法時,并不通過消息派發機制,而是直接調用其底層C語言版本,這樣性能更好。
使用ARC時必須遵循的方法命名規則
若方法名以下列詞語開頭,則其返回的對象歸調用者所有:
若方法名不以上述四個詞語開頭,則表示其所返回的對象不歸調用者所有。
變量的內存管理語義
在應用程序中,可用下列修飾符來改變局部變量與實例變量的語義:
通過__weak局部變量可以打破由塊所引入的“保留環”。
ARC如何清理實例變量
ARC會借助OC特性來生成清理例程,來自動清理OC對象,所以ARC環境下通常無需再編寫dealloc方法
不過如果有非OC的對象,那么仍然需要清理,dealloc方法可以這樣寫:
在dealloc方法中只釋放引用并解除監聽
對象在經歷其生命期后,最終會為系統所回收,這時就要執行dealloc方法了。dealloc方法中要做的主要就是釋放對象所擁有的引用。如果對象擁有其他非OC對象,那就要手工釋放。
dealloc方法中還要把配置的觀測行為清理掉,比如用NSNotificationCenter訂閱的通知,dealloc可以這樣寫:
有一些開銷較大或系統內稀缺的資源不在dealloc中釋放,比如文件描述符、套接字、大塊內存等。通常的做法是:實現另一個方法,當對象使用完后,就調用此方法。
這種對象所屬的類,接口可以這樣寫:
#import <Foudation/Foudation.h>
@interface EOCServerConnection : NSObject
- (void)open:(NSString*)address;
- (void)close;
@end
該類與開發者的約定是:想打開連接,就調用“open:”方法,連接完畢就調用close方法。
close與dealloc方法可以這樣寫:
#import <Foudation/Foudation.h>
@interface EOCServerConnection : NSObject
- (void)open:(NSString*)address;
- (void)close;
@end
還要注意:dealloc中不要隨便調用其他方法,執行異步任務的方法不應在dealloc里調用,只能在正常狀態下執行的方法不應在dealloc里調用,屬性的存取方法不應在dealloc里調用。
編寫“異常安全代碼”時留意內存管理問題
在try塊中,如果先保留了某個對象,在釋放它以前又拋出了異常,那么除非catch塊能處理此問題,否則對象所占內存就將泄漏。
在手動管理引用計數時,要使用@finally塊,并在塊中釋放對象,這樣無論是否拋出異常,其中的代碼都保證會運行且只運行一次。
當自動管理引用計數時,需要打開-fobjc-arc-exceptions標志,該標志默認關閉,只有在打開時,才會自動管理異常的引用計數,不過降低運行效率。
當遇到大量異常捕獲操作時,就要考慮重構代碼,用NSError式錯誤信息傳遞法來取代異常。
以弱引用避免保留環
最簡單的保留環就是兩個對象互相引用對方。
保留環會導致內存泄漏
如果只剩一個引用還指向保留環中的實例,而現在又把這個引用移除,那么整個保留環就泄漏了。
避免保留環最佳方式是弱引用,這種引用經常用來表示“非擁有關系”,將屬性聲明為unsafe_unretained或weak即可。
兩者都會對屬性弱引用,區別只是對象回收后的行為:
以“自動釋放池塊”降低內存峰值
嵌套自動釋放池,可以控制應用的內存峰值,使其不致過高。
比如這段代碼:
如果方法創建臨時對象,那這些對象即便不再使用,也依然存活,直到系統將其釋放并回收,意味著所有臨時對象都要等for循環執行完才會釋放。這會導致程序所占內存量持續上漲。
當循環長度取決于用戶輸入時更如此,比如:
這時,增加一個自動釋放池,把循環內的代碼包裹在“自動釋放池塊”中,那么循環中自動釋放的對象就會放在這個池,內存峰值就會降低。
用“僵尸對象”調試內存管理問題
Cocoa提供了“僵尸對象”這一功能,當功能啟用后,系統會把所有已經回收的實例轉化為“僵尸對象”,而不會真正回收它們。這種對象內存不可能遭到覆寫,并且收到消息后會拋出異常,準確說明發送過來的消息,并描述回收之前的那個對象。僵尸對象是調試內存管理問題的最佳方式。
將NSZombieEnabled環境變量設為YES,即可開啟此功能。開啟功能后,系統會修改對象的isa指針,指向特殊的僵尸類,從而使對象變為僵尸對象,他會相應所有選擇子,響應方式為:打印一條包含信息內容及其接收者的消息,然后終止應用程序。
不要使用retainCount
ARC中已經廢棄了retainCount這一方法,而在不啟用ARC時,也不應該使用這一方法。
這個方法之所以無用,原因在于:它所返回的保留計數只是某個給定時間點上的值。
塊與大中樞派發
理解“塊”這一概念
塊可以實現閉包
塊的基礎知識
塊與函數類似,只不過是直接定義在另一個函數里的,和定義它的函數共享同一個范圍內的東西,用符號“^”來表示,后面跟一對花括號。
塊類型的語法結構如下:
塊的強大之處是:在聲明它的范圍內,所有變量都可以為其所捕獲,但是默認情況下,不可以在塊里修改。
如果聲明時加上__block修飾符,這樣就可以在塊內修改了。
如果塊捕獲了對象類型,就會自動保留它,就會導致一個問題,塊本身也具有引用計數,這樣就很容易導致循環引用。
塊的內部結構
棧的內存布局如圖:
這里invoke變量是個函數指針,指向塊的實現代碼,descriptor是指向結構體的指針,結構體中聲明了塊對象的總體大小,還聲明了copy與dispose兩個輔助函數對應的函數指針。
全局塊、棧塊及堆塊
定義塊時,所占內存區域是分配在棧中的,離開相應范圍后,棧內存可能被覆寫。
將塊對象發送copy消息就可以拷貝,把塊從棧復制到堆。此后再調用copy,只會遞增塊對象引用計數
除了棧塊和堆塊,還有全局塊,這種塊不會捕捉任何狀態,也不需要,塊所使用的內存區域在編譯器就已經確定了。這種塊可以聲明在全局內存中,不需要在棧中創建。
全局塊的拷貝是個空操作,它實際上相當于單例。
為常用的塊類型創建typedef
塊類型的語法結構如下:
為了簡化,可以使用typedef為塊起一個已讀的別名,比如這樣:
現在可以直接使用新類型了。
當塊作為參數時,也可以簡化,比如:
簡化后:
使用類型定義還有個好處,就是在打算重構塊時,會很方便,只要修改類型定義處即可。
用handler塊降低代碼分散程度
為用戶界面編碼時,經常異步執行任務,如果使用委托模式,那么處理數據的代碼總是在受委托者處實現,如下圖:
而如果改用塊來寫的話,代碼就會變得更加清晰。
如此一來,代碼就變得好懂許多了。
除此之外,委托模式還有一個缺點:如果類要分別使用多個獲取器下載不同數據,那就得在delegate回調方法里根據傳入參數來切換。
如果使用塊,就可以將實現代碼與委托者內聯在一起。
在設計API時,如果用到了handler塊,那就可以增加一個參數,使調用者可以通過此參數來決定應該把塊安排在哪個隊列上執行。
用塊引用其所屬對象時不要出現保留環
使用塊很容易導致“保留環”
有兩種常見情況。第一種如下例:
當使用block捕獲self時,就可能造成這種情況,這時應該等閉包執行完后,再打破保留環。
另一種情況是互相引用,這時常見的方法是不保留引用,設定一套機制,令獲取器對象自己設法保持存活。
多用派發隊列,少用同步鎖
在GCD出現之前,有兩種辦法來實現同步機制,第一種是“同步塊”(@synchronized),第二種是直接使用NSLock對象。這兩種方法都有其缺陷,替代方案就是GCD。
有種簡單而高效的辦法可以代替同步塊或鎖對象,那就是使用“串行同步隊列”。GCD在底層實現。可以做許多優化。
比如屬性的存取:
將屬性的訪問操作都放在串行隊列中,就可以實現同步。
如果把設置方法改為異步派發,就可以進一步優化:
然而,獲取方法與設置方法不能并發執行,但多個獲取方法可以并發執行。將并發隊列與柵欄塊結合,可以進一步優化。
在隊列中,柵欄塊必須單獨執行,不能與其他塊并行。
實現代碼也很簡單:
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
- (NSString*)someString {__block NSString *localSomeString;dispatch_sync(_syncQueue, ^{localSomeString = _someString;});return localSomeString;
}
?
- (void)setSomeString:(NSString*)someString{dispatch_barrier_async(_syncQueue, ^{_someString = someString;});
}
多用GCD,少用performSelector系列方法
使用performSelector方法,可以動態綁定消息,如下圖:
但是這種方法不僅可能導致內存泄漏,還很難傳達一些非對象類型參數。
避免這些限制和缺陷,方法就是使用塊,而perform方法提供的那些線程功能,都可以通過在大中樞派發機制中使用塊來執行,延后執行可以用dispatch_after來實現,在另一個線城上執行任務可以通過dispatch_sync及dispatch_async來實現。
掌握GCD及操作隊列的使用時機
有時使用NSOperationQueue要比使用GCD技術要更合適。
用NSOperationQueue類的“addOperationWithBlock:”方法搭配NSBlockOperation類使用操作隊列,語法與GCD非常類似,但使用下來有以下好處:
通過Dispatch Group機制,根據系統資源狀況來執行任務
Dispatch group是GCD的一項特性,能夠把任務分組。
與dispatch group相關的有以下幾個函數:
dispatch_group_tdispatch_group_create// 創建分組
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_bloack_t block);//管理分組
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);//管理分組
有兩個方法可以用于等待dispatch group執行完畢:
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
二者的區別是:前者會阻塞當前線程,而后者只會在完成任務后進行通知。
未必一定要使用dispatch group,使用異步派發加單個隊列也可以實現該效果。然而,使用dispatch group,GCD會根據系統資源狀況來調度這些任務。若不采用group,則不僅性能有所下降,還需要額外代碼來防止死鎖。
使用dispatch_once來執行只需運行一次的線程安全代碼
在GCD中,有一種很好的方式實現單例,采用以下函數:
void dispatch_once(dispatch_once_t *token, dispatch_block_t block);
此函數接受一個參數,只要這個參數相同,那么這個塊里的代碼便只執行一次。
可以這樣實現單例:
這個參數應該聲明在static或global作用域中。
不要使用dispatch_get_current_queue
dispatch_get_current_queue這個函數已經被棄用了。使用這個函數時,容易造成死鎖。并且這個函數的行為也總是與預計不符。因為隊列存在一套層級體系。
所以使用該函數檢查當前隊列是否為執行同步派發所用的隊列,并不總是奏效。這個函數所返回的不一定是API指定的那個,有可能是API內部的那個同步隊列。
為了解決這個問題,最好的辦法是通過GCD提供的功能來設定“隊列特有數據”,通過隊列特有數據來解決不可重入導致的死鎖。
系統框架
熟悉系統框架
許多系統框架可以直接使用,最重要的是Foundation與CoreFoundation。
許多常見任務都能用框架來做,例如音頻與視頻處理、網絡通信、數據管理等。
多用枚舉塊,少用for循環
遍歷collection有四種方式,最基本的是for循環,其次是NSEnumerator類和for...in...快速便利法,最新、最先進的是“塊枚舉法”。
以數組為例,NSArray中定義了這個方法
- (void)enumerateObjectsUsingBlock:(void(^)id object, NSUInteger idx, BOOL *stop)block
通過這種方法遍歷collection,可以直接從塊里獲取更多信息。遍歷數組時可以知道當前下標,遍歷字典可以同時獲取鍵值,還可以修改塊的方法簽名以免進行類型轉換操作。
對自定義其內存管理語義的collection使用無縫橋接
使用無縫橋接技術,可以在Foudation框架中的OC對象與CoreFoudation框架中的C語言數據結構之間來回轉換。
使用無縫橋接,可以將collection轉換為具備特殊內存管理語義的collection。
比如字典的鍵為“拷貝”,而值為“保留”,這時就只能使用無縫橋接來改變語義。
CF框架中的可變字典叫CFMutableDictionary,可通過下列方法來制定鍵和值的內存管理語義:
構建緩存時選用NSCache而非NSdictionary
構建緩存時,應選用NSCache而非NSdictionary。
NSCache的優勢在于:當系統資源快要耗盡時,它可以自動刪減緩存。
NSCache對象可以設置上限來限制緩存中的對象總個數和成本。
NSCache經常與NSData和NSPurgeableData搭配使用。將NSPurgeableData與NSCache搭配使用,可以實現自動清除數據的功能。
精簡initiakize與load的實現代碼
類的初始化有兩個方法,分別是load和initialize
load方法的問題在于,執行時系統處于“脆弱狀態”,在執行時必然執行超類的load方法,如果依賴程序庫,那庫里所有相關類的load方法必定會先執行,但是根據某個庫,無法判定各個類的載入順序。因此,在load方法里使用其他類是不安全的。
load方法務必實現精簡一些,因為整個程序在執行load時都會阻塞。
initialize方法會在程序首次調用該類之前調用且只調用一次。它與load不同之處在于:
1.initiakize為惰性調用
2.執行initialize時,系統處于正常狀態,也能確保一定是“線程安全的環境”中執行。
3.如果某個類為實現initialize,就會運行超類的實現代碼。
initialize方法在實現時也盡量精簡。
別忘了NSTimer會保留其目標對象
使用重復執行模式的計時器,很容易引入“保留環”
如果創建本類的實例,并調用startPolling方法,創建計時器時,由于目標對象是self,所以要保留此實例。然而,實例也保留了計時器。因此就產生了保留環。
這時可以拓展NSTimer的功能,用“塊”來打破保留環。不過需要創建分類,將實現代碼加入分類中。
- (void)startPolling{__weak EOCClass *weakSelf = self;_pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block: ^{EOCClass *strongSelf = weakSelf;[strongSelf p_doPoll];} repeats:YES];
}
這段代碼先定義了一個弱引用,令其指向self,然后捕獲這個引用,而不是原本的self變量,這樣self就不會被計時器所保留塊開始執行時,立即生成strong引用,以保證實例在執行期間持續存活。