close

本章學習目標:   

1. 了解iOS中記憶體管理的基本概念。

2. 了解記憶體管理的慣用設計及使用方式。

 

不管是新手還是老手,iOS的記憶體管理都是一件非常重要的事,相較於Java、.NET之類有自動記憶體回收的語言,在iOS中沒有處理好記憶體的管理的後遺症要嚴重非常非常多,一般來說,若iOS寫出來的程式常常莫明奇怪當掉的話,大部分的情況只有兩種原因:一種是多執行緒的存取管理不當,另一個就是記憶體沒有管理好造成違規存取或是吃光系統記憶體之類的問題,因此了解這一章的內容,將對寫出一個穩定的程式有相當大的幫助。

 

大致上我們對iOS的記憶體管理將會著重在兩個部分,第一個部分是Core Foundation,這部分的程式碼都是以C語言實作的,另一個部分則是 Cocoa Touch,這個部分則是使用Objective-C來實作的,不過雖然分成這兩部分,但是很多情況下這兩個類別的概念是共通的,更進一步地來說,Core Foundation和 Cocoa Touch在很多類別甚至是一體兩面,在Core Foundation的某些類別,例如字串CFString和Cocoa Touch裏的NSString,它們骨子裏根本是同一個東西,只要動一些小手腳就可以將這個個類別互換而且在使用上完全沒有問題,在這一章我們將會分別為這兩大類別的記憶體管理做初步的介紹。

 X.1 Core Foundation的記憶體管理

 Core Foundation相關的記憶體管理主題,大致可以由以下幾個方面來探討:

 

l   記憶體配置器(Allocator)。

l   擁有權的準則。

l   Core Foundation物件的生命週期。

l   拷貝函數(Copy Function)。

 

X.1.2記憶體配置器(Allocator)

記憶體配置器負責為我們配置和釋放記憶體,基本上我們在大部分的情況下不需要直接管理記憶體的配置,重置或是釋放Core Foundation的物件,我們只需直接將記憶體分配器直接丟入負責產生物件的函數中即可,而這些函數的名字中通常都會有「create」字眼,例如:「CFStringCreateWithPascalString」等,這些負責建造物件的函數會使用傳入的記憶體配置器來配置記憶體並產生物件。

 

X.1.3 擁有權的準則

因為在程式中我們常會對物件進行存取、建立和釋放等等動作,為了確保記憶體不會被誤用,Core Foundation為存取和建立物件定義了一些使用準則。

 

X.1.3.1 基本原則

在試著了解Core Foundation的記憶體管理時,試著去使用「擁有權」來理解或許是個不錯的方式,每一個物件都有一或多個的擁有人,我們使用保留計數(retain count,和參考計數reference count的功能是一樣的)來記錄一個物件有多少個擁有人。當一個物件沒有擁有人(即retain count為0),系統就會釋放它。Core Foundation定義了下面的準則來表示物件的擁有和釋放:

 

l   如果建立了一個物件(包含直接建立或是其他物件拷貝而來)則擁有這個物件。

l   如果由其他地方取得一個物件,我們並不擁有這個物件,若想要預防這個物件被釋放,則必須成為這個物件的擁有人之一(使用 CFRetain)。

l   如果我們是一個物件的擁有人,當我們不再使用這個物件時,我們有責任要釋放這個物件(CFRelease)。

 

X.1.3.2 命名準則

 

Core Foundation的命名準則提供我們一個了解物件擁有權的規則,一般來說Core Foundation和我們自己建立的類別都應該要遵守這些準則才不會讓使用者感到混淆進行造成不必要的錯誤,不過在某些情況下我們仍是有機會遇到不遵守這些準則的開發人員,因此如果情況允許的話我們仍必須參考文件上的說明以避免意外發生,不過一般來說了解這些命名準則並依此來使用物件是必要的。在Core Foundation中,有以下兩個關鍵字告訴我們此時拿到的物件是具有擁有權的,這兩個關鍵字分別是:

 

l   Create

l   Copy

 

如果函數的名稱是包含「Get」則表示我們沒有擁有權。

依據上面的準則,可以得到下面的表格:

 

關鍵字

 

Create

取得擁有權。

Copy

取得擁有權。

Get

沒有擁有權。

 

接下來的章節我們將會為這些概念做進一步的介紹。

 

 

X.1.3.3 建立物件的規則(Create Rule)

 

以下函數的名稱告訴我們擁有在Core Foundation取得的物件:

 

l   含有「Create」字眼的物件的建立函數。

l   含有「Copy」字眼的物件複製函數。

 

再次強調,當我們擁有一個物件,在不再需要使用該物件時就有責任使用「CFRelease」函數來釋出我們的擁有權,千萬記得要有始(Create、Copy)有終(Release),絕對不能始亂終棄,否則到後來可能會發生許多無法預期的麻煩,下面一些範例是擁有「Create」字眼的函數:

 

CFTimeZoneRef CFTimeZoneCreateWithTimeIntervalFromGMT(CFAllocatorRef allocator, CFTimeInterval ti);

CFDictionaryRef CFTimeZoneCopyAbbreviationDictionary(void);

CFBundleRef CFBundleCreate(CFAllocatorRef allocator, CFURLRef bundleURL);

 

第一個函數在名字中含有「Create」字眼,這個函數負責建立「CFTimeZone」物件(用來代表時區的物件),而我們擁有這個物件,所以我們有責任要釋放擁有權。

 

第二個函數在名字中含有「Copy」字眼,這個函數建立了一個包含時區物件各個屬性的複本,我們依舊擁有這個物件,所以有責任要釋放它。

 

第三個函數我們就不再多做解釋了,總之使用完後記得要將它釋放掉。

 

註:在這個範例子,細心的讀者會發現回傳的物件都有REF做為結尾字串,這些以REF為結尾的物件實際上是一個指向物件的指標而不是物件的本身,因此在很多情況下我們必須確保指標指向的內容是有效的才不會造成錯誤地存取已經被系統釋放的物件。

 

 

X.1.3.4 取得物件的規則 (Get Rule)

如果在Core Foundaton中使用「Copy」或是「Create」之外的函數如「Get」類函數取得物件,我們將不會擁有這個物件,而這個物件的生命週期也不在掌控之中,因此,若此時要確保物件的存在,我們必須使用「CFRetain」來宣告這個物件的擁有權,然後在不需要使用時再釋放它,以下面的例子CFAttributedStringGetString來說:

 

CFStringRef CFAttributedStringGetString(CFAttributedStringRef aStr);

 

這個函數僅會回傳一個不帶有擁有權的字串,因此我們無法得知這個字串真正的生命週期,如果這個取回的字串在之後被它的擁有人釋放掉,這個字串就有可能會因為沒有擁有人而被系統把資源釋放掉,此時若再去存取,程式就會當掉;為了要避免這種意外,若我們要確保字串不會消失就要使用CFRetain宣告擁有權,然後在不再使用時使用CFRelease將它釋放掉,不然的話,這個物件將永遠不會被系統釋放而造成記憶體遺漏(memory leak)。

 

X.1.4 管理Core Foundation 物件的生命週期

 

Core Foundation物件的生命週期由它的參考計數來(refernece count)決定,參考計數是物件內部用來記錄還有多少人想要確保這個物件存在的一個變數。當我們藉由建立或是拷貝而來的新物件,它們的參考計數會設定為1,接下來的使用者可以藉由CFRetain來將參考計數加1,再靠CFRelease來將參考計數減1,當物件的參考計數為0時,這個物件的記憶體配置器(Allocator)就會將物件佔用的記憶體釋放掉。

 

註:參考計數的英文是reference count,不過在iOS的開發中,我們比較常看到的字眼是retain count,中文通常翻成保留計數,不過這兩個就我們的狀況來說,意思是一樣的,因此在本書中不管我們使用的是reference count或是retain count,它們指的是同一件事。

 

X.1.4.1 增加物件的參考計數(Retaining Object References)

若將想要增加參考計數,請將Core Foundation物件的參考傳CFRetain函數中,請參考下面的範例:

 

    /* myString 是一個由其他地方取得的 CFStringREF */ 

    myString = (CFStringRef)CFRetain(myString);

 

 

X.1.4.2 減少物件的參考計數(Releasing Object References)

若將想要減少參考計數,請將Core Foundation物件的參考傳CFRelease函數中,請參考下面的範例:

 

CFRelease(myString);

 

註:基本上我們不應該直接將物件佔有的記憶體釋放掉(使用freee函數),在不需使用物件時,請直接使用CFRelease,然後將記憶體管理的部分交由系統來處理。

 

X.1.4.3 取得物件的參考計數

我們可以使用CFGetRetainCount函數來取得物件目前的參考計數:

 

CFIndex count = CFGetRetainCount(myString);

 

一般來說,除非是為了偵錯,我們很少會需要知道物件目前的參考計數,如果你發現自己可能會需要知道參考計數,那代表我們的程式在某些部分沒有確實遵守擁有權的準則。

 


 

X.2 Cocoa Touch 的記憶體管理

 

相較於Core FoundationCocoa Touch的記憶體管理更顯得重要,因為在大部分的情況下所有iOS的程式幾乎都和Cocoa Touch脫不了關係,Cocoa Touch的記憶體管理和Core Foundation類似,也都是使用擁有人的概念,也是使用參考計數等等,在這一小節我們將會對Cocoa Touch的記憶體管理做一些初步的介紹。

 

X.2.1 記憶體管理準則

 

Cocoa Touch記憶體管理的基本原則和Core Foundation是類似的:

 

如果你使用包含有以下關鍵字的方法來取得物件,那麼你將擁有這個物件:

 

關鍵字

範例

   alloc

alloc

   new

newObject

copy

mutableCopy

 

如果對物件呼叫retain也會擁有這個物件;

 

同樣地就像在Core Foundation時的情形一下,當我們擁有物件時就有責任在不需要時呼叫它的release或是autorelease來釋出擁有權,至於在其他的情況下取得的物件,我們絕對不能主動釋放它,否則隨時都有可能讓程式意外地當掉。

 

下面準則是基本上述準則的延伸:

在大部分的情況下,若我們想要將某一個物件做為某個實體變數的屬性,我們必須使用retain或copy來確保物件的存在,唯一的例外是在某些狀況下必須使用弱連結的方式,我們將在稍後提到這種連結方式。

autorelease 表示這個物件會在稍後收到release訊息,表示這個物件在離開目前的方法之後,隨時都有可能被系統釋放(釋放的時機通常會是在目前執行緒結束的時候)。

除非物件在多執行緒的情況下會被存取,否則在一般的情況下,由某個方法中取得的物件,我們可以確定在目前的方法乃是有效的,這個物件也能安全地回到呼叫我們的方法中。

 

註:在Core Foundation的命名規則並不一定適用於Cocoa Touch中,例如create在Cocoa Touch中並不會取得擁有權,因此不需要釋放該物件,例如:

MyClass * myInstance = [MyClass createInstance];

這個例子中,因為我們沒有myInstance的擁有權,所以不必在之後呼叫它的release。

 

X.2.2 物件的擁有和釋放

 

Cocoa Touch的物件使用參考計數來管理物件的生成和消失,整體的概念和前面提到的擁有權的取得和釋放相當地類似,在Cocoa Touch中這些準則如下:

你擁有你所建立的物件。

我們使用名字含有alloc、new或copy的方法來產生物件。

你可以使用retain來取得物件的擁有權。

一個物件可以有多個擁有人,若你需要確保某個物件不會消失,你可以主動取得它的擁有權。

你擁有的物件必須在不需使用後,將它釋放掉。

我們可以使用release或是autorelease來釋放擁有的物件。

你不能釋放不屬於你的物件。

這項應該不用多作解釋了吧,用猜的也該猜得到了。

 

請參考下面的例子:

 

    MyClass * myClass = [[MyClass alloc] init];

    NSArray * myAttributes = [myClass attributes];

    [myClass release];

 

在這個例子中,我們使用alloc來產生myClass物件,因此當我們不再使用myClass之後,必須呼叫myClassrelease來釋放它,而我們是由myClass中取得myAttributes,我們並沒有建立myAttributes因此我們最後並不需要呼叫myAttributesrelease來釋放它。

 

X.2.3 參考計數

 

Cocoa Touch的擁有權準則依賴保留計數(和參考計數是一樣的意思)來實現,我們稱之為「retain count」,每個物件都有自己的retain count,以下是retain count運作的方式:

 

l   當你建立一個物作時,它的retain count為1。

l   當你呼叫物件的retain時,它的retain count會加1。

l   當你呼叫物件的release時,它的retain count會減1。

l   當你呼叫物件的autorelease時,它的retain count會在未來某個時間被減1。

l   當物件的retain count為0時,它將會呼叫自己的dealloc並且釋放所佔用的資源。

 

X.2.4 Autorelease

在NSObject中定義了autorelease這個方法,autorelease提供我們延遲物件release的管道,當我們對一個物件呼叫autorelease後,表示在目前的程式範圍(scope)之後不再需要這個物件了,而這個範圍的大小則是由目前的autorelease pool來決定,也就是說,當我們呼叫物件的autorelease之後,物件會暫時被保留,直到目前的autorelease pool結束時才會被釋放,而此處的釋放指的是呼叫物件的release方法,若呼叫release後物件的retain count不是0,則物件所佔用的記憶體仍不會被釋放掉。

 

註:一般的情況下,autorelease pool會在每個執行緒開始時建立,而在執行緒結束時消失。

 

延續在上一個例子中的attributes,我們可以實作如下:

 

– (NSArray *) attributes {

NSArray *array = [[NSArray alloc] initWithObjects:main, auxiliary, nil];

return [array autorelease];

}

 

這個例子中,因為我們用alloc來建立array所以擁有這個陣列,在稍後必須釋放它,在這即是使用autorelease

 

當其他的方法取得attributes這個陣列後可以假設這個陣列將會在沒有擁有人之後被系統釋放,但是在它目前的範圍是有效的,它甚至能將這個陣列傳給呼叫它的人。

 

下面列出兩個誤用的例子:

  1. 下面的例子會造成記憶體遺漏(memory leak)

 

//錯誤範例請勿模仿

– (NSArray *) attributes {

NSArray *array = [[NSArray alloc] initWithObjects:main, auxiliary, nil];

return array;

}

 

上面這個範例中的陣列有效範圍只在attributes方法內,當這個方法結束後這個物件將可能沒有任何人參考到它而造成沒有人能釋放它,根據前面介紹的命名準則,使用這個方法的人並不知道它擁有這個方法回傳的物件,因此並不會主動去釋放它,如此將會造成記憶體遺漏。

 

  1. 下面的錯誤範例回傳了一個無效的物件

 

//錯誤範例請勿模仿

– (NSArray *) attributes {

NSArray *array = [[NSArray alloc] initWithObjects:main, auxiliary, nil];

[array release];

return array; //此處的陣列是無效的

}

 

在上面的例子中,物件在alloc後馬上被呼叫release這時物件的retain count因為為0(alloc時retain count為1,release時減1,結果為0),所以系統會馬上釋放這個物件,因此最後這個方法會回傳一個已經被系統釋放的物件。

 

我們可以將上面兩個範例修改成下面這個正確的範例:

 

– (NSArray *) attributes {

NSArray *array = [NSArray arrayWithObjects:main, auxiliary, nil];

return array;

}

 

如此一來,我們在這個方法中沒有擁有array這個物件,因此不必負責release它,而且可以放心地將它回傳出去。

 

註:上例中的arrayWithObjects裏的實作方式即是呼叫物件的autorelease之後再回傳給我們,

 

X.2.5 共用物件的有效範圍

一般來上,上面介紹的物件有效範圍適用於大部分的情況,沒有意外的話,當我們拿到物件時,直接回傳回去應該是不會有問題的,不過有些例外,這些例外大致分成兩種:

 

  1. 當存在於容器類別中的物件被移除時:

heisanObject = [array objectAtIndex:n];

[array removeObjectAtIndex:n];

// heisanObject 現在變成無效了.

 

當物作被容器類別移除時(在本例中是一個陣列),它會送一個release給要被移除的物件,若此時容器類別是此物件的唯一擁有者,被移除的物件將會馬上被系統釋放(在本例中是heisanObject)。

 

  1. 當父物件被系統釋放時:

id parent = <#建立一個父物作#>;

// ...

heisanObject = [[parent child] ;

[parent release];

// heisanObject 現在變成無效了.

 

在某些情況下我們由某個物件A取得了物件B,然後系統將物件A釋放掉,若此時物件A是物件B的唯一擁有者,那麼物件B則有可能馬上隨著物件A被系統釋放掉,這是因為物件A是物件B的擁有者,所以在消失前要釋放掉物件B,若物件A呼叫的是release,則物件B會馬上被系統釋放,若是呼叫autorelease,則是稍後被系統釋放。

 

要避免上面的錯誤,我們必須retain heisanObject物件,然後再不需要使用後再呼叫它的release,如下:

heisanObject = [[array objectAtIndex:n] retain];

[array removeObjectAtIndex:n];

// 使用 heisanObject.

[heisanObject release];

 

X.2.6 存取方法 (Accessor Methods)

 

如果你的類別中有一個物件的變數,那麼你必須確保這個物件在你需要存取它時都是存在的,因此我們必須要取得物件的擁有權,同時在使用完後釋放它。

 

舉個來說,如果你有一個屬性可以設定,這個設定方法的內容如下:

 

– (void)setAttribute:(Attribute *)newAttribute

{

[myAttribute autorelease];

my = [newAttribute retain]; /* 將新的屬性保留起來. */

return;

}

 

在上面的例子中,因為我們使用retain來記住新的屬性,所以這個物件是別人共用的,也就是說在別的地方可能會有人和共同使用這個物件,如果需要擁有自己的物件,我們應該要使用copy來將這個物件建立拷貝下來:

 

– (void)setAttribute:(Attribute *)newAttribute

{

[myAttribute autorelease];

my = [newAttribute copy]; /* 將新的屬性拷貝起來. */

return;

}

 

以上兩個方法都會呼叫原來物件的autorelease,所以使用起來沒有問題,但因為autorelease是延後系統釋放物件的時機,而有時我們會希望物件能儘早將不用的資源釋放掉,所以會將autorelease改成release,這樣只要沒有人在使用舊的屬性的話,舊屬性佔用的資源就可以即時被釋放掉,但是這樣又會引申出另一個問題,也就是當新屬性和舊屬性為同一個物件時,一經release有可能馬上被系統釋放掉,這樣之後的retain或是copy就會造成系統當掉(因為存取已經被系統釋放的資源),為了避免新屬性和原來屬性是同一個物件而不小心被系統釋放掉,因此我們將新的實作改下以下寫法來避免這種錯誤發生:

 

– (void)setAttribute:(Attribute *)newAttribute

{

      if(newAttribute != myAttribute)

{

              [myAttribute release];

             myAttribute = [newAttribute retain]; /*或是使用copy */

}

return;

}

 

在上面所有的例子中,我們都沒有將myAttribute釋放掉,這是因為在這些例子中myAttribute是我們物件的一個屬性,這時myAttribute必須在物件dealloc中釋放掉,否則就會造成記憶體的遺漏。

 

X.2.7 釋放物件佔用的資源(Deallocating an Object)

 

當物件的retain count0時,它所佔用的記憶體就會被系統回收,我們將這種行為稱為freed或是deallocated,如果你的類別中擁有自己的物件,那麼你就必須實作dealloc方法,並在裏面將這個物件釋放掉,請參考下面的範例:

 

- (void)dealloc

{

     [mainAttribute release];

     [auxiliaryAttribute release];

     [super dealloc];

}

 

註:dealloc將自動由系統呼叫,我們不應該主動呼叫物件的dealloc。

 

 

註:就字面上來說releasedealloc都有釋放的意思,因此剛開始學習的時候可能會感到困惑,基本上在這兩個最主要的差別是release指的是將retain count1,而dealloc則是真正的釋放佔用系統的記憶體,因此,關於這兩者正常的流程是我們使用release來減少物件的retain count,當retain count減到0的,dealloc就會被呼叫並將物件佔用的記憶體釋放掉,還給系統。

 

X.2.7以參考方式傳回的物件(Objects Returned by Reference)

 

在Cocoa Touch中有些方法將會傳回物件的參考(也就是 ClassName ** 或是 id *),常見的例子就是NSError,通常許多函數使用這個物件將錯誤訊息回傳給呼叫的人,例如:

 

類別

 方法

NSData

- (id)initWithContentsOfURL:(NSURL *)url options:(NSDataReadingOptions)readOptionsMask error:(NSError **)errorPtr;

NSString

- (id)initWithContentsOfURL:(NSURL *)url encoding:(NSStringEncoding)enc error:(NSError **)error;

 

 

依據前面介紹的規定,我們並沒有產生NSError這個物件所以並不擁有它,因此也不必要去釋放它,在下面的例子中,我們取回的NSError並不用去release它。

 

NSString *fileName = <#檔案名稱#>;

NSError *error = nil;

NSString *string = [[NSString alloc] initWithContentsOfFile:fileName

encoding:NSUTF8StringEncoding error:&error];

if(string == nil)

{

      // 錯誤處理 ...

}

// ... 做一些和這個string相關的事

[string release];

 

X.2.8 循環參照

 

在某些情況下,兩個物件可能會互相參考,也就是說,這兩個物件都有一個實體變數指向對方,舉個例來說,一個文字處理器的Document物件(用來表示整份文件),建立了許多個Page物件(用來表示每一個頁面),而每Document物件會記住每一個Page的參考,然後每個Page也必須有一個變數來記住Document物件,然後Page物件又有類似的情況也建立了許多Paragraph物件(用來表示段落),如果這些物件,都使用retain來確保對方的存在時,就會造成循環參照(Cyclical reference),這時以Document 和 Page的關係來說,Document必須要Page 消失後才會消失,而Page又要等Document消失後才會消失,這樣的結果就會造成兩個人永遠都沒有辦法消失(因為兩者的retain count永遠不會為0)。

 

為了要解決這個問題,我們必須為這類的物件建立階層性的架構,以此例來說,Document必須是整份文件的根節點(root node),而Page則為Document 的子節點(child node),換句話說Document為Page的父節點(Parent node),然後只有父節點retain子節點,子節點只使用弱連結和父節點進行連結,這樣就可以解決這個問題,請參考下面圖示來表示這個問題:

 

註:弱連結(weak link),在這指的是沒有retain對方物件,僅記住對方的參考,若對方消失我們並不會知道。

 

 

 

X.3 自動回收池(Autorelease pool)

 

Cocoa Touch中了解autorelease pool的運作是很重要的一件事,在這一節中我們將對autorelease pool進行初步的介紹。

 

X.3.1 自動回收池概觀

Autorelease poolNSAutoreleasePool的一個實體,autorelease pool中裝著那些收過autorelease訊息的物件,當autorelease dealloc時,它會對在它裏面所有物件發送release訊息,物件每放進autorelease pool一次,在結束時就會被呼叫一次release,換句話說每呼叫一次autorelease,最後的效果等同於呼叫同樣次數的release,只不過這些release會延後被呼叫的時機,因此,使用autorelease來取代release可以延長物件的生命週期,因為物件會等到autorelease消失時才會真的被系統釋放掉。

 

系統使用一個Stack來管理Autorelease pool,當我們產生一個 autorelease pool物件時,系統會將這個物件push到這個stack中,當autorelease pool dealloc時,系統會將這個物件由stack中移出,當一個物件收到autorelease訊息時,它將會被放到目前stack最上面的autorelease pool中,也就是最後放進stack的那一個autorelease pool

 

一般來說在程式起始的main函數中一定會有一個autorelease pool,請參考任何一個專案中的main.m,它的內容大致如下:

 

int main(int argc, char *argv[]) {

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    int retVal = UIApplicationMain(argc, argv, nil, nil);

    [pool release];

    return retVal;

}

 

我們可以看到main函數的第一行就是在建立autorelease pool,而這個pool將在呼叫[pool release]時被系統釋放掉,一般來說我們不必自己去管理autorelease pool,下面是幾個例外的情況,在這些情況下我們必須處理autorelease pool的生成和結束。

 

-          你在寫一個command line的程式,這時系統不會自動幫你進行autorelease pool的支援。

-          當你產生一個新的執行緒,你必須在執行緒開始時建立自己的aurorelease pool

-          當你在使用迴圈撰寫程式,而這個迴圈中使用了許多暫存的物件,你可以建立autorelease pool來管理這些物件,這樣這些物件就可以提早被釋放,讓系統不會累積太多不再使用的物件(否則這些物件會延後被釋放)

 

此外,在使用autorelease pool時要注意的就是,autorelease必須在方法中直接使用,絕對不能將autorelease pool變成某個物件的實體變數。

 

 

待續

arrow
arrow
    全站熱搜

    穿越時空的旅人 發表在 痞客邦 留言(0) 人氣()