2015年11月12日 星期四

[iOS] kkbox-ios-dev note 記憶體管理 Part1

如果用完之後不釋放記憶體,就會造成軟體佔用了一堆沒有用到的記憶體,
記憶體用量愈來愈大,造成記憶體用盡,在iOS上系統會強制終止我們的
應用程式,這種狀況叫做記憶體漏水(Memory Leak)。

如果一塊記憶體已經被釋掉了,我們卻還認為這塊記憶體還存在我們
可以呼叫的物件,所以當我們嘗試呼叫的時候,才發現這塊記憶體該存在的物件
已經不在了,這種狀況叫做over-release或是invalid memory reference,
會造成應用程式crash,crash log上會告訴你錯誤類型是EXC_BAD_ACCESS。

記憶體自動回收(Garbage Collection,GC),在軟體執行時,如果發現已經
沒有任何一個變數指向某塊記憶體,就代表這塊記憶體再也用不到,於是開始
回收這塊記憶體。90年代後誕生的程式語言,幾乎都有GC機制。

蘋果在Mac OSX 10.5實作GC時問題不小、是後來逐步將Compiler從GCC換成
LLVM,最後決定改變技術方向,從另外一種方向來解決將記憶體管理自動化的
問題,就是在iOS5上推出的ARC(Automatic Reference Counting),不在
runtime回收記憶體,而是在編譯程式的時候,自動幫你加上與記憶體釋放相關
的程式碼。而蘋果的下一步,就是直接在ARC的基礎上開發新的程式語言Swift。

在Objective-C語言發展之初,就建立了一套計算有多少地方用到某個物件的
簡單機制,叫做reference count,意義非常簡單:只要一個物件被某個地方
用到一次,這個地方就對這個物件加一,反之就減一,如果數字減

[anObject retain]; // +1
[anObject release]; // -1
NSLog(@"Retain count:%d", [anObject retainCount]); //檢查某個物件被retain了幾次

所謂的auto-release其實也沒有多麼自動,而是說,在這一輪run loop中我們先
不釋放這個物件,讓這個物件可以在這一輪run loop中都可以使用,但是先打上
一個標籤,到了下一輪run loop開始時,讓runtim判斷有那些前一輪runloop中被
標成是auto-release的物件,這個時候才減少retain count決定是否要釋放物件。

在為立Foundation物件的時候,除了可以呼叫「alloc」、「init」、以及「new」
(「new」這個method其實就相當於呼叫了「alloc」與「init」、比方說,
我們呼叫[NSObject new],就等於呼叫了 [[NSObject alloc] init],
之外還可以呼叫另外一組與物件名稱相同的method。

以NSString為例,有一個叫做initWithString的instance method,就有一個對應的
class method叫做stringWithFormat,使用這一組method就會產生auto-release
的物件。也就是說,呼叫了[NSString stringWithFormat:...],就相當於呼叫了
[[[NSString alloc] initWithFormat:...] autorelease]。使用這一組method,可以
讓程式碼較為精簡。

基本原則
1. 如果是「init」、「new」、「copy」這些method產生出來的物件,用完就該
release
2. 如果是其他一般method產生出來的物件,就會回傳auto-release物件、或是
Singleton,就不需要另外呼叫release

而呼叫retain與release的時機包括:
*如果是在一般程式碼中用了某個物件,用完就要release或是auto-release。
*如果是要將某個Objective-C物件,變成是另外一個物件的成員變數,
就要將物件retain起來,但是delegate物件不該retain。
*在一個物件被釋放的時候,要同時釋放自已的成員變數,也就是要在實作
dealloc的時候,釋放自已的成員變數。

要將某個物件設為另外一個物件的成員變數,需要寫一組getter/setter。

Getter/Setter與Property語法

Getter就是用來取得某個物件的某個成員變數的method,
Setter則是用來設定成員變數。


@interface MyClass : NSObject
{
    int number;
}

- (int)number;
- (void)setNumber:(int)intNumber;

@end

我們建立了setter叫做setNumber而getter叫做number。
在其他語言的慣例中,getter可能會取名叫做getNumber但是在Objective-C
則是只取number這樣的名稱,實作則是:
- (int)number {
    return number;
}

- (void)setNumber:(int)intNumber {
    number = intNumber;
}

如果是Objective-C物件,我們則要將原本成員變數已經指向的記憶體位置
釋放,然後將傳入的物件retain起來。
- (id)myVar {
    return myVar;
}

- (void)setMyVar:(int)intMyVar {
    [myVar release];
    myVar = [intMyVar retain];
}

假如今天我們在開發的應用程式中用到了很多thread,而在不同的thread中 同時會用到myVar,這麼寫其實並不安全:在某個thread中呼叫了[myVar release] 之後,到myVar指定到intMyVar的位置之間,假使另外一個thread剛好用到了myVar 這時候myVar剛好指到了一塊已經被釋放的記憶體,於是就告成了EXC_BAD_ACCESS錯誤。 要避免這種狀況,一種方法是加上一些lock讓程式在呼叫setMyVar:的時候,不讓其他 thread可以使用myVar:另外一種簡單的方法是,只要一直不要讓myVar指定到可能被釋放的記體位置。我們可以這麼寫:
- (void)setMyVar:(int)intMyVar {
    id tmp = myVar;
    myVar = [intMyVar retain];
    [tmp release];
}

我們先將myVar原本指向的記憶體位置,暫存在一個變數中,接著直接將myVar指到傳入的 記憶體位置,接著再釋放tmp變數中所記住的、原本的記憶體位置。由於每次都要這麼寫, 寫久了會覺得麻煩,通常會寫成一個macro,或是直接使用property語法。 用property的語法可寫成:
@interface MyClass : NSObject
{
     id myVar;
    int number;
}
@property (nonatomic, retain)id myVar;
@property (nonatomic, assign)int number;

@end

@implementation MyClass
@synthesize myVar;
@synthesize number;

- (void)dealloc {
    [myVar release];
    [super dealloc];
}

@end
我們在這邊使用了@synthesize語法,在編譯我們的程式的時候, 其實就會被編譯成我們在上面所寫的getter/setter,而我們想要設定myVar的內容時, 除了可以呼叫setMyVar:之外,也可以呼叫dot語法,像是myObject.myVar = someObject 我們需要注意,在釋放記體的時候,myVar = nil 與 self.myVar = nil這兩段程式是 不一樣的,前者只是單純的將myVar的指標指向nil,但是並沒有釋放原本所指向的記憶體位置 所以會造成記憶體漏水,但後者卻等同於呼叫[self setMyVar:nil],會先釋放myVar原本指向的位置 然後將myVar設成nil。在Xcode4.4之後,如果用了property語法,我們甚至不用宣告對應的成員變數, compiler在編譯程式的時候,會自動補上myVar與number需要對應的成員變數。

沒有留言:

張貼留言