標籤:
移動app開發中,由於行動裝置記憶體的限制,記憶體管理是一個非常重要的話題。objective-c的記憶體管理,不僅是面試當中老生常談的一個必問話題,也是日常項目開發中,特別需要重視的環節。對於筆者這種以java語言入門編程世界的開發人員來說,習慣了垃圾收集器的自動化管理,對於oc的引用計數器管理方式,還是需要花功夫來學習和運用。
1. ARC 和 非ARC
oc的記憶體管理方式,分為ARC(automatic reference counting自動引用計數)和非ARC模式。Apple 在 Xcode 4.2 中發布了 Automatic Reference Counting (ARC),該功能為開發人員消除了手動執行引用計數的負擔。
目前xcode建立項目,都會推薦預設的ARC方式(ARC的確有很大的優勢)。當然,如果必須要使用非ARC,可以在build setting中修改automatic reference counting選項為NO。如果在ARC項目中引入非ARC的代碼或者靜態庫,需要在build phases設定相應資源為-fno-objc-ar;相反,非arc項目設定arc使用-fobjc-arc。
ARC與非ARC,從字面上來看,在於是否auto,即是由編譯器來自動實現reference counting,還是由開發人員手動完成引用計數的加減。作為現在經常使用arc模式來開發的我們來說,ARC大大減少了我們對記憶體管理的工作,甚至,很多入門開發人員完全像開發java應用一樣,沒有管object的釋放。然而對oc來說,從mac os x10.8開始,garbage collector也已廢棄,iOS上壓根就沒有出現過這個概念。iOS上oc的記憶體管理,本質上是引用計數的管理,理解引用計數,是iOS記憶體管理的核心。
2. 引用計數
如果一個對象沒有被其他對象所引用,則表明該對象已不需要,即可以釋放。這就好比人們在一張飯桌上吃飯,如果飯桌上還有人,就表明該飯桌還不可以收拾;只有當所有人都吃好飯離開,使用人數小於1,才表明這張飯桌已使用完,可以清理收拾。 所以對象釋放的時機,即是該記憶體的引用計數小於1. oc中的記憶體管理方式,就是基於對引用計數的管理。
ARC模式下,編譯器完成了引用計數的管理,開發人員不需要手動添加引用計數管理的代碼(實際上也不允許),所以以下主要通過非ARC模式的代碼方式,解釋引用計數管理方式。
1》對象的初始化和釋放
oc中的object需要繼承自統一的父類NSObject。對於一個NSObject的生命週期來說,關注以下幾個方法:
- alloc
- new
- copy
- mutableCopy
- dealloc
當調用上述前四種方法時,都會產生新的對象,object的引用計數都會+1,歸調用者所有(即需要調用者釋放)。系統對象以及庫中所有的對象都會遵循這個規則,即所有以上述四種方法名開頭的方法,都會使引用計數+1;反之,所有不以該命名開頭的返回對象方法,都不會使引用計數+1.對於ARC來說,這種規則被確立為硬性規定。當我們自己自訂對象時,也應遵循這種規範,雖然通過命名規則來體現記憶體管理方式有些讓人奇怪。
dealloc是對象內部的實現方法,當object的引用計數為0,即沒有被其他對象引用時,該方法會調用,釋放object。需要注意的是,在每個對象的生命週期內,dealloc只會調用一次。然而當引用計數為0時,並不能保證系統何時執行該方法。運行期系統會在何時時機調用dealloc,所以決不應該手動調用dealloc,一旦調用後對象就不可用。
在非ARC對象的dealloc方法中,主要就是釋放對象所擁有的引用,除此之外,通常還需要把原來配置的觀測行為清理掉,如remove掉kvo和notification的觀察者。對於ARC模式來說,並不允許我們直接複寫dealloc這些記憶體管理相關的操作。編譯器會利用Objective-c++的特性,為C++對象的.cxx_destruct方法產生代碼,釋放對象。但是,對於那些通過malloc()產生的記憶體或者比如CoreFoundation中的對象,並非屬於oc對象,開發人員需要按照需要自己釋放。
在非ARC模式中,我們可以手動控制引用計數的增減。通過以下兩個方法:
當調用[object retain],引用計數+1;調用[object release]時,引用計數-1.當object不在使用,需要釋放時,調用release使引用計數清0,而不要直接調用dealloc。
NSObject協議中,存在retainCount這個方法,用於查詢對象的當前保留計數:
- (NSUInteger)retainCount
然而它並沒有卵用。然而它並沒有卵用(重要的事要說兩次)。在ARC模式中,記憶體管理相關的方法都無法通過編譯,而在非ARC中,retainCount很多時候並不能反映出對象正常的保留計數。甚至當對象已被釋放的情況下,再次調用到retainCount會直接導致crash。
NSString *str = @"12456"; NSLog(@"str retainCount: %lu",(unsigned long)[str retainCount]); NSNumber *num = @1; NSLog(@"num retainCount: %lu",(unsigned long)[num retainCount]); NSNumber *numF = @3.1415f; NSLog(@"numF retainCount: %lu",(unsigned long)[numF retainCount]);
以上代碼的結果可能會讓你大跌眼鏡:
2015-07-24 14:37:19.238 TestMemory[10987:3418823] str retainCount: 42949672952015-07-24 14:37:19.241 TestMemory[10987:3418823] num retainCount: 182015-07-24 14:37:19.241 TestMemory[10987:3418823] numF retainCount: 1
str和num對象的retainCount出現了讓人不解的數字。實際上編譯器對這些對象都做了最佳化,這種最佳化只在某些場合才會發生,所以numF對象的retainCount是正常的。所以再次強調,不要試圖利用retainCount來做任何操作。
iOS的記憶體管理,說白了也就是對於引用計數,調用retain和release來告知系統對記憶體的釋放。我們舉個栗子,以下代碼在非ARC模式中運行:
/*! * 飯桌 */ @interface Table : NSObject@end@implementation Table- (void)dealloc{ NSLog(@"%s",__func__); [super dealloc];}@end /*! * 客人 */@interface Guest : NSObject@property (nonatomic, copy) NSString *name;@property (nonatomic, retain) Table *table;- (instancetype)initWithName:(NSString *)name;@end@implementation Guest- (instancetype)initWithName:(NSString *)name{ self = [self init]; if (self) { self.name = name; } return self;}- (void)dealloc{ NSLog(@"%@ dealloc",self.name); [self.name release]; [self.table release]; [super dealloc];}- (void)setTable:(Table *)table{ [table retain]; [_table release]; _table = table;}@end
//兩位客人到一張飯桌上吃飯的過程再現 Table *table = [[Table alloc] init]; NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); Guest *guest_1 = [[Guest alloc] initWithName:@"guest_1"]; NSLog(@"%@ retain count: %lu",guest_1.name,(unsigned long)[guest_1 retainCount]); guest_1.table = table; NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); Guest *guest_2 = [[Guest alloc] initWithName:@"guest_2"]; NSLog(@"%@ retain count: %lu",guest_2.name,(unsigned long)[guest_2 retainCount]); guest_2.table = table; NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); [guest_1 release]; NSLog(@"%@ retain count: %lu",guest_1.name,(unsigned long)[guest_1 retainCount]); NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); [guest_2 release]; NSLog(@"%@ retain count: %lu",guest_2.name,(unsigned long)[guest_2 retainCount]); NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); [table release]; NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]);
以上實現了一個簡單的客人到飯桌吃飯的情境:首先有準備出一張飯桌,來了一位客人1,做到飯桌吃飯,又來了一位客人2,到飯桌吃飯,客人1吃好飯走人,客人2走人,飯桌使用完畢,收拾飯桌。
首先看兩個類,Table很簡單,類中沒有保留的其他對象,dealloc方法調用[super dealloc]保證父類對象的釋放。Guest類中,添加了兩個屬性(@property),屬性的修飾符是copy和retain。iOS會對property自動建立get和set方法,而copy/retain/assign等這一類修飾符,表明了set方法中對屬性值不同記憶體管理的方式,這一點後文詳述。這裡只要知道,copy和retain都會導致set方法中將屬性值保留,即retainCount+1。Guest類中加入了自訂的init方法,注意init開頭的方法,必須按照oc的代碼規範要求才能實現init時調用的目的,如例子中initWithName:這樣的camel-case。dealloc方法中實現了對保留對象的釋放,最後調用[super dealloc]。setTable:方法複寫了property的set方法,實際上只是將retain修飾符屬性值的預設set方法用代碼再現了一下。可以看到,對於retain的屬性值,會先保留新值,後釋放舊值,然後將新值賦給屬性,實現對象的保留。而set方法的調用者,需要管理所賦值的釋放。
以上代碼的log如下:
2015-07-24 10:14:07.623 TestMemory[10956:3397055] table retain count: 12015-07-24 10:14:07.628 TestMemory[10956:3397055] guest_1 retain count: 12015-07-24 10:14:07.629 TestMemory[10956:3397055] table retain count: 2 -> table被guest對象保留,引用計數+12015-07-24 10:14:07.630 TestMemory[10956:3397055] guest_2 retain count: 12015-07-24 10:14:07.631 TestMemory[10956:3397055] table retain count: 3 -> table被guest對象保留,引用計數+12015-07-24 10:14:07.632 TestMemory[10956:3397055] guest_1 dealloc -> guest_1 引用計數為為0,觸發dealloc2015-07-24 10:14:07.632 TestMemory[10956:3397055] guest_1 retain count: 12015-07-24 10:14:07.633 TestMemory[10956:3397055] table retain count: 22015-07-24 10:14:07.634 TestMemory[10956:3397055] guest_2 dealloc -> guest_2 引用計數為為0,觸發dealloc2015-07-24 10:14:07.634 TestMemory[10956:3397055] guest_2 retain count: 12015-07-24 10:14:07.635 TestMemory[10956:3397055] table retain count: 12015-07-24 10:14:07.636 TestMemory[10956:3397055] -[Table dealloc] -> table 引用計數為0,觸發dealloc2015-07-24 10:14:07.636 TestMemory[10956:3397055] table retain count: 1
可以看到,對象通過alloc方法create後,retainCount都會+1。table對象在alloc,又分別被guest_1和guest_2保留,導致retainCount為3。guest調用release方法後,引用計數為0,都會觸發guest對象的dealloc方法,導致table對象引用計數-1.最後table調用release,引用計數為0,觸發table對象dealloc。
然而,正如上所說,retainCount在這裡並沒有很好地起到說明引用計數的作用。雖然引用計數都以為0,最後的retainCount都沒能顯示為0.這可能是運行期內部對retainCount方法做出了處理,或者是對象記憶體並沒有被立即釋放,否則對象dealloc後再次調用已釋放對象的方法都會導致EXC_BAD_ACCESS的crash發生。
2》屬性的所有權語義(ownership semantic)
待續...
iOS記憶體管理(objective-c)