iOS記憶體管理
主要參考資料:《Effective Objective-C 2.0》,《Objective-C進階程式設計 iOS與OS X多線程和記憶體管理》
在學習記憶體管理的時候,查閱了不少資料,零零散散的記錄在有道雲筆記中,在這裡總結提煉一下,希望在友善自己檢視的同時能幫助到大家。
1.引用計數
在引用計數架構下,每個對象都有個可以遞增或遞減的計數器,用以表示目前有多少個事物想令此對象繼續存活下去。這在OC中叫做“保留計數(retain count)”,不過也可以叫做“引用計數”。如果想使某個對象繼續存活,那就遞增其引用計數;用完了之後,就遞減其引用計數。計數變為0,就表示沒人關注此對象了,于是就可以把它銷毀。
MRC:在手動引用計數模式下,NSObject協定聲明了下面三個方法用于操作計數器,retain,release,autorelease。
- (instancetype)retain OBJC_ARC_UNAVAILABLE;
- (oneway void)release OBJC_ARC_UNAVAILABLE;
- (instancetype)autorelease OBJC_ARC_UNAVAILABLE;
ARC:蘋果的官方說明:
在OC中采用ARC機制,讓編譯器來進行記憶體管理。在新一代Apple LLVM編譯器中設定ARC為有效狀态,就無需再次鍵入retain或release代碼,這在降低程式崩潰、記憶體洩露等風險的同時,很大程度上減少了開發程式的工作量。編譯器完全清楚目标對象,并能立即釋放那些不再被使用的對象。如此一來,應用程式将具有可預測性,且能流暢運作,速度也将大幅提升。
雖然現在項目中推薦使用ARC,但是了解MRC是非常有用的。
2. 引用計數式記憶體管理的思考方式
讓我們先忽略ARC,來考慮引用計數式記憶體管理的思考方式。
- 自己生成的對象,自己所持有
- 非自己生成的對象,自己也能持有
- 不再需要自己持有的對象時釋放該對象
- 非自己持有的對象無法釋放
2.1 自己生産的對象,自己所持有
使用以下名稱開頭的方法名意味着自己生成的對象自己持有:在OC中方法名能展現出記憶體管理語義,以上面這4個名稱開頭的方法名表示“生成的對象自己持有”,也可以說成“生成的對象歸調用者所有”。“生成的對象自己持有”意味着:調用上述四種方法的那段代碼要負責釋放方法所傳回的對象。
- alloc
- new
- copy
- mutableCopy
{
//調用[[NSObject alloc]init]方法會在堆上産生一個對象,并且沒有被release和autorelease。這個堆上的對象目前的引用計數為1
//把這個對象的指針指派給obj。指派不會導緻引用計數的變化,相當于通過obj可以拿到這個堆上的對象使用了。
id obj = [[NSObject alloc]init];
//這個代碼段快要結束了,把這個堆上的對象釋放掉,不然會産生記憶體洩漏。
[obj release];
}
2.2 非自己生成的對象,自己也能持有
用alloc/new/copy/mutableCopy以外的方法取得的對象,因為非自己生成并持有,是以自己不是該對象的所有者。
//取得非自己生産并持有的對象
//調用[NSMutableArray array]方法,會在堆上産生一個對象,這個對象被調用了autorelease方法,它會再下一次運作循環的時候被release。可以把這個對象想象成引用計數為0的對象(雖然是過一段時間才變為0),隻有retain一下,才能持有這個對象。
id obj = [NSMutableArray array];
//取得的對象存在,但自己不持有對象
//NSMutableArray對象被賦給變量obj,但變量obj自己并不持有該對象。使用retain方法可以持有對象。
//取得非自己生産并持有的對象
id obj = [NSMutableArray array];
//取得的對象存在,但自己不持有對象
[obj retain];
//自己持有對象
通過retain方法,非自己生成的對象跟用alloc/new/copy/muableCopy方法生成并持有的對象一樣,成為了自己所持有的。
2.3 不再需要自己持有的對象時釋放該對象
自己持有的對象一旦不再需要,持有者有義務釋放該對象。釋放使用release方法。
//自己生産并持有對象
id obj = [[NSObject alloc]init];
//自己持有對象
[obj release];
//釋放對象,指向對象的指針仍然被保留在變量obj中,貌似能夠通路,但對象一經釋放絕對不可通路。
如此,用alloc方法由自己生成并持有的對象就通過release方法釋放了。
用alloc/new/copy/mutableCopy方法生産并持有的對象,或者用retain方法持有的對象,一旦不再需要,務必要用release方法進行釋放。
如果要用某個方法生成對象,并将其返還給該方法的調用方,那麼它的源代碼又是怎樣的呢?
- (id)allocObject
{
//自己生成并持有對象
id obj = [[NSObject alloc]init];
//自己持有對象
return obj;
}
如上例所示,原封不動地傳回用alloc方法生成并持有的對象,就能讓調用方也持有該對象,請注意allocObject是符合命名規則的。
allocObject名稱符合前文的命名規則,是以它與用alloc方法生成并持有的對象的情況完全相同,是以使用allocObject方法也就意味着“自己生成并持有對象”。
那麼,調用[NSMutableArray array]方法使取得的對象存在,但自己不持有對象,又是如何實作的呢?根據命名規則,不能使用以alloc/new/copy/mutableCopy開頭的方法名,是以使用object這個方法名。
-(id)object
{
id obj = [[NSObject alloc]init];
//自己持有對象
[obj autorelease];
//取得的對象存在,但自己不持有對象
return obj;
}
上例中我們使用了autorelease方法。使用該方法,可以使取得的對象存在,但自己不持有對象。autorelease提供這樣的功能,使對象在超出指定的生存範圍時能夠自動并正确地釋放(調用release方法)。
autorelease是把對象注冊到自動釋放池,等pool結束時自動調用release方法。
使用NSMutableArray類的array類方法等可以取得誰都不持有的對象,這些方法都是通過autorelease而實作的。此外,根據命名規則,這些用來取得誰都不持有的對象的方法名不能以alloc/new/copy/mutableCopy開頭,這點需要注意。
id obj1 = [obj0 object];
//取得的對象存在,但自己不持有
//當然,也能通過retain方法将調用autorelease方法取得的對象變為自己持有。
id obj1 = [obj0 object];
//取得的對象存在,但自己不持有
[obj1 retain];
//通過retain使得自己持有對象
2.4 非自己持有的對象無法釋放
對于用alloc/new/copy/mutableCopy方法生成并持有的對象,或是用retain方法持有的對象,由于持有者是自己,是以在不需要該對象時需要将其釋放。而由此以外所得到的對象絕對不能釋放。倘若在應用程式中釋放了非自己所持有的對象就會造成崩潰。
//自己生成并持有對象
id obj = [[NSObject alloc]init];
//自己持有對象
[obj release];
//對象已釋放
[obj release];
//釋放之後再次釋放已非自己持有的對象,應用崩潰
或者在“取得的對象存在,但自己不持有對象”時釋放
id obj1 = [obj0 object];
//取得的對象存在,但自己不持有對象
[obj1 release];
//釋放了非自己持有的對象,這肯定會導緻應用程式崩潰
幫助了解:
使用與持有是兩回事。你可以使用這個對象,但是你并不是它的所有者。比如有個對象的所有者已經對它進行了autorelease,你可以使用這個對象,但是除非你進行了retain,否則你不是它的所有者。
從所有權的角度考慮記憶體管理問題,就能很容易解釋為什麼需要在description方法中向傳回的NSString對象發送autorelease消息:
因為該隻是建立了一個NSString對象,但是并不想擁有該對象。建立NSString的對象隻是為了傳回一個結果,将NSString對象“交出去”而已。交出去後,别人愛擁有就擁有,不愛擁有就算了。
3. ARC
實際上“引用計數式類存管理”的本質部分在ARC中并沒有改變。就像“自動引用計數”這個名稱表示的那樣,ARC隻是自動地幫助我們處理“引用計數”的相關部分。在編譯機關上,可設定ARC有效或無效,這一點便能佐證上述結論。比如對每個檔案可選擇使用或不使用ARC。
上述的記憶體管理思考方式在ARC中依然有效,隻是在源代碼的的書寫上有些不同。到底有什麼樣的變化呢?首先要了解ARC中追加的所有權聲明。
3.1所有權修飾符
OC程式設計中為了處理對象,可将變量類型定義為id類型或各種對象類型。
所謂對象類型就是指向NSObject這樣的OC類的指針,例如NSObject*。id類型用于隐藏對象類型的類名部分,相當于C語言中常用的“ void* ”。
ARC有效時,id類型和對象類型同C語言其他類型不同,其類型上必須附加所有權修飾符。所有權修飾符一共有4種。
- __strong
- __weak
- __unsafe_unretained
- __autoreleasing
3.1.1 __strong修飾符
__strong修飾符是id類型和對象類型預設的所有權修飾符。
id obj = [[NSObject alloc]init];
該obj變量實際上被附加了__strong所有權。相當于:
id __strong obj = [[NSObject alloc]init];
再看如下的代碼段:
{
//ARC
id __strong obj = [[NSObject alloc]init];
}
此源代碼明确指定了C語言的變量的作用域。ARC無效時,該源代碼可記述如下:
//ARC無效
{
id obj = [[NSObject alloc]init];
[obj release];
}
如此源代碼所示,附有__strong修飾符的變量obj在超出其變量作用域時,即在該變量被廢棄時,會釋放其被賦予的對象。
如strong這個名稱所示,__strong修飾符表示對對象的“強引用”。持有強引用的變量在超出其作用域時被廢棄,随着強引用的失效,引用的對象會随之釋放。
3.1.2 __weak修飾符
有些時候會出現循環引用的問題:
{
Dog* dog1 = [[Dog alloc]init];//狗對象;dog1持有狗對象的強引用
Cat* cat1 = [[Cat alloc]init];//貓對象;cat1持有貓對象的強引用
[dog1 setObj:cat1];//狗對象裡的成員變量obj_持有貓對象的強引用
[cat1 setObj:dog1];//貓對象裡的成員變量obj_持有狗對象的強引用
//現在狗對象的持有者是dog1、貓對象的成員變量obj
//現在貓對象的持有者是cat1、狗對象的成員變量obj
}
//因為dog1,cat1超出作用域,強引用失效,是以自動釋放狗對象和貓對象,引用計數從2變為1
//此時兩個對象中的成員互相引用兩個對象,發生記憶體洩露
像下面這種情況,雖然隻有一個對象,但在該對象持有其自身時,也會發生循環引用(自引用)。
id test = [[Test alloc]init];
[test setObject:test];
使用__weak修飾符可以避免循環引用。弱引用不能持有對象,對對象的引用計數沒有影響。還是那句話,可以使用但不持有,持有者把它release了,你就不能使用了。把之前的成員變量的修飾符改成__weak即可解決循環引用問題。
__weak修飾符還有另一個優點。在持有某對象的弱引用時,若該對象被廢棄,則此弱引用将自動失效且被指派為nil。通過檢查附有__weak修飾符的變量是否為nil,可以判斷被指派的對象是否已廢棄。
3.1.3 __unsafe_unretained修飾符
附有_unsafe_unretained修飾符的變量同附有__weak修飾符的變量一樣,因為自己生成并持有的對象不能繼續為自己所有,是以生成的對象會立即被釋放。到這裡,__unsafe_unretained修飾符和__weak修飾符是一樣的。
__unsafe_unretained修飾的變量與__weak有點差別是:在對象被廢棄時,不會被指派為nil。
3.1.4 __autorelease修飾符
用附有__autoreleasing修飾符的變量替代autorelease方法。
前面說到,用alloc/new/copy/mutableCopy以外的方法取得的對象,因為非自己生成并持有,是以自己不是該對象的所有者。
我們自己編寫不以這些開頭的方法時,編譯器會按照規則幫我們在要傳回的對象上調用autorelease方法。
有種情況:
比如NSData中的一個方法:
我們需要聲明一個
NSError *error;
然後把
&error
傳入方法中。
這裡的
(NSError **)errorPtr
,其實等同于
(NSError * __autoreleasing *)errorPtr
這樣就會把*error加入到自動釋放池中。之是以這樣做,是為了符合記憶體管理的思考方式,作為alloc/new/copy/mutableCopy方法傳回值取得的對象是自己生成并持有的,其他情況下便是取得非自己生成的對象。是以,使用附有__autoreleasing修飾符的變量作為對象取得參數,與除alloc/new/copy/mutableCopy外其他方法的傳回值取得的對象完全一樣,都會注冊到autoreleasepool,并取得非自己生成并持有的對象。