高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

Set to Nil After Release

image

iOS的記憶體管理真的不少東西要學,在Objective-C裡有時候會看到一些程式碼這樣寫:

1
2
3
4
5
6
7
-(void) someMethod
{
  [mybook release];
  mybook = nil;

  [super dealloc];
}

先把它release掉了,然後下一行再把它指向nil。nil跟其它程式語言裡的null或none是一樣的東西,就是nothing、虛無飄渺、沒有任何東西的意思。

問題是,這個nil是必要的嗎? 不指向nil會出什麼包嗎?

先來釐清一個觀念:對一個物件發送release訊息並不會直接把物件佔用的記憶體還給記憶體,release這個動作只是減少retain count而已,當retain count變成0的時候會自動啟動dealloc,這時候才是真正的被消滅掉。

在Objective-C裡所有的物件變數都是指標,指向某一塊配置的記憶體,所以,下面這行程式碼:

1
mybook = nil;

意思是把mybook這個變數指向nil。這樣說來,是不是也不用什麼release了,直接把變數設定成nil不就好了?

我們先來看個例子:

1
UIImage *img = [[UIImage alloc] init];

這應該不陌生了,這建立一個名為img的指標變數,並指向某一個UIImage的實體。如果我在它下面再加一行:

1
img = [[UIImage alloc] init];

就是再做一次alloc/init,讓剛剛那個img變數指向這個新的實體。看起來很直覺,但指標的東西跟我們一般寫的程式的習慣不同,當你把img指向另一個新的實體,原來舊的實體並不會消失,它會存在記憶體裡,只是沒有人指向它,所以也沒有人能再存取它。當然在有支援GC的環境上,系統會自動回收那些沒人要的東西,但在不支援GC的環境,那個沒人要的孩子就會一個人孤獨的漂流著了,所以如果你要這樣做,請記得先把原來的那顆給release掉,再給它一顆新的。

那如果我再接著這樣做:

1
img = nil;

把img指向nil並不會減少retain count,那剛剛前面我產生的第二個物件就會跟第一個物件一樣,它不會消失,它只是在記憶體空間裡漂流,當然它佔用的記憶體也不會還給系統。所以請切記,如果你要把物件給丟掉,不要直接把變數設成nil,請使用release或autorelease

回到最一開始的問題,我的答案是:其實是沒什麼需要一定要在release之後再弄個nil給它,但這樣做是個好習慣。

那什麼時候會出問題?

在別的程式語言裡,當你要存取null物件的時候,通常會直接跳錯誤給你看,但在Objective-C裡如果你對nil送訊息的,即使是沒定義的method,它也只會靜靜的不回應而已。所以,回到最前面的程式碼來看,當你把mybook給release之後,也許有那個萬一的情況下,有別的method要存取它,但它可能因為已經被dealloc掉了,所以就會爆炸了(發生機會很小,但不代表永遠不會發生)。但如果把mybook指向nil,即使有人要再來存取這個mybook也不會出現錯誤了。

以上,我對記憶體管理的東西的了解相當有限,所以如果以上內容有誤還請不吝指正 :)

Protocol in Objective-C

image

前面提到了OOP的繼承,但不像C++可以有多重繼承,Objective-C是單一繼承的,如果想要做到一個類別同時擁有多種型別的能力,可以透過實作其它型別的interface來達成這個目的。在Java/AS3是用”interface”這個關鍵字,在Objective-C則是用”@protocol”。(有寫過Java/AS3的要特別注意不要把interface跟protocol搞混了,在Objective-C的interface等於Java/AS3的class,而protocol則是相當於interface)

直接來看看要怎麼做吧。如果你要新增一個自定的protocol的話,可以直接在你的專案裡新增一個protocol檔:

image

當然,你要全部寫在一起也沒人反對,只是為了模組化以及以後的可重複使用考量,建議獨立出來另外寫。新增完成之後(它是一個header檔),就可以開始來寫了,程式碼如下:

1
2
3
4
5
6
@protocol Drawable

-(void) draw;
-(void) changeColor;

@end

在Objective-C裡的protocol是用@protocol這個語法來定義的。在上面這段程式碼裡,我放了兩個方法,但沒有寫內容。接下來如果我要實作自這個protocol的話,所有定義在@protocol裡的方法都得實作出來。另外,在Objective-C 2.0之後加了@required@optional的語法,可以讓你設定這個method是不是必需一定要實作的項目。用法如下:

1
2
3
4
5
6
7
8
9
10
@protocol Drawable

@required
-(void) draw;
-(void) changeColor;

@optional
-(void) whateverMethod;

@end

如果沒特別標明的,預設是@required。如果你要實作這個protocol的話,照英文字面來看,@required的部份是規定要實作的,@optional的話就隨你高興了。要注意的是@required跟@optional這兩個語法的影響範圍,是從它以下所有的method都會被影響,直到另一個directive或是@end為止,所以如果你要省略@required的話,記得那些method要寫在@optional前面。接下來來看看要怎麼實作這個protocol:

1
2
3
4
5
6
7
8
9
10
#import <Cocoa/Cocoa.h>
#import "Drawable.h"

@interface Book : NSObject <Drawable>
{
  int price;
}
@property int price;

@end

實作protocol的方法就是用”<>”標記,裡面放protocol的名稱。並不限定只能實作一個protocol,如果要實作多個protocol的話,則是用逗點分開:

1
@interface Book : NSObject <Drawable, Openable>

因為到目前為止,我們都還沒實作那個protocol裡定義的方法,所以這時候如果直接按下Build的話,就會跳出警告訊息:

image

接著來把該做的填一填吧。因為在protocol的地方已經有定義好了方法,所以在@interface的地方就不用再特別寫一次,只要在@implementation裡補上該實作的方法就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// --------------
// interface
// --------------
#import <Cocoa/Cocoa.h>
#import "Drawable.h"

@interface Book : NSObject <Drawable>
{
  int price;
}
@property int price;

@end

// --------------
// implementation
// --------------
#import "Book.h"
@implementation Book

@synthesize price;

// 實作方法draw
-(void) draw
{
  NSLog(@"draw me!");
}

// 實作方法changeColor
-(void) changeColor
{
  NSLog(@"change color!");
}

@end

如果你實作了所有@required的方法的話,則稱為遵守(conform)或採納(adopt)這個protocol(硬翻成中文還是覺得怪怪的,還是英文比較簡潔直接)。若要檢查某物件是否有乖乖遵守某個protocol的規定:

1
2
3
4
5
Book *book = [[Book alloc] init];
if ([book conformsToProtocol:@protocol(Drawable)] == YES)
{
  NSLog(@"the book is conform to Drawable protocol");
}

protocol本身也可以像一般類別的繼承,例如:

1
2
3
4
5
6
7
@protocol A
-(void) methodA;
@end

@protocol B <A>
-(void) methodB;
@end

這時如果你要實作protocol B,則methodA跟methodB都需要實作。

另外,你也可以把protocol拿來當一般的型別定義來用,例如:

1
id <Drawable> some_object;

表示說這個some_object是個有實作Drawable這個protocol的物件,在編譯階段就可以先做型別檢查。當然也可以一次多個,一樣用逗點分開:

1
id <Drawable, Openable> some_object;

上面提到的這種用@protocol來定義方法的,稱做formal protocol,從名字看大概猜得出來一定也有叫做informal protocol的東西,不過這個會在category的部份再做說明。

@class directive in Objective-C

前面幾篇有提到了在Objective-C裡的OOP要怎麼寫,也知道要怎麼在@interface裡設定instance variable(以下簡稱ivar),雖然範例程式碼裡只是用了int這個簡單型態,但實際上更常看到的是在A類別裡有放一個B類別的物件的情況(Composition)。通常你如果要用到一個A類別的時候,你會用:

1
#import "A.h"

的方法把header檔給import裡來,但是來假想一下下面這個情境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// A類別裡有放了一顆B ivar
@interface A : NSObject
{
  B *b;
}

@end

@implementation A
@end

// B類別裡有放了一顆A ivar
@interface B : NSObject
{
  A *a;
}
@end

@implementation B
@end

當然舉這個例子是比較極端啦,這樣的情況應該很少發生。如果你跑一下以上的的程式碼,會得到錯誤訊息”expected specifier-qualifier-list before ‘B’”,意思就是說這個B類別並不存在。A跟B兩個類別互相有需要,但B還沒出生,變成是雞生蛋蛋生雞的問題,這時候就可以用上@class這個語法了。只要在@interface A前面加一行:

1
@class B;

這行就是告訴編譯器說「這個B是一個類別,至於這個B類別的內容你不用管,反正遲早會告訴你,你先用就是了」,這樣一來就可以順利通過編譯了。不過寫了@class不代表就可以不用實作那個類別的內容,如果到時候沒有import B的header進來或是沒有實作B類別,硬是要用的話在編譯過程就會跳出一個訊息”warning: receiver ‘B’ is a forward class and corresponding @interface may not exist”。

優點:

  1. 跟#import語法比起來,用@class的編譯速度比較快。
  2. 可解決互相參照的問題。

缺點:

其實我也不知道這樣會有什麼缺點

Property and Synthesize

image

上篇,因為在類別裡,instance variable(以下簡稱ivar)預設是protected的,也就是說只有該類別以及它的子類別才能存取它,如果要給外部使用的話,則需要來幫它加個setter/getter。但每次只為了一個ivar就要寫一對的setter/getter也太麻煩了,在Objective-C 2.0之後加入了@property@synthesize的語法,讓這個工作簡單多了。借用上一篇的例子:

1
2
3
4
5
6
7
8
9
10
@interface Book : NSObject
{
  int price;
}

-(int) price;
-(void) setPrice: (int) p;
+(void) printBookInfo

@end

如果改用@property來寫:

1
2
3
4
5
6
7
8
9
@interface Book : NSObject
{
  int price;
}

@property int price;
+(void) printBookInfo;

@end

原來的setter/getter就可以省下來不用寫,然後在@implementation的部份則是使用@synthesize語法:

1
2
3
4
5
6
7
8
9
10
@implementation Book
// 這個@synthesize語法幫忙產生setter/getter
@synthesize price;

+(void) printBookInfo
{
  NSLog(@"Hello, This is a book");
}

@end

這裡的@synthesize price,其實就相當於自動產生了我們在上一篇寫的那一對setter/getter的程式碼:

1
2
3
4
5
6
7
8
9
-(int) price
{
  return price;
}

-(void) setPrice: (int) value
{
  price = value;
}

這樣程式碼就簡潔許多了。雖然@synthesize會自動幫忙產生setter/getter,但如果你想要寫自己的setter/getter,或是想要額外添加功能在裡面,只要照著它預設生成的setter/getter方法命名規則就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// interface
@interface Book : NSObject
{
  int price;
}

@property int price;
+(void) printBookInfo;

@end

// implementation
@implementation Book

@synthesize price;

// 自定setter
-(void)setPrice:(int)p
{
  // 故意讓傳入值變2倍
  price = p * 2;
}

+(void)printBookInfo
{
  NSLog(@"Hello, This is a book");
}
@end

@synthesize雖然會自動幫你建立一對setter/getter,但還是會以你建立的為主。

另外,記得在別的程式語言裡可以用點”.”來存取物件的屬性嗎? Objective-C 2.0之後也可以這樣做了,直接看看語法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Book *b = [[Book alloc] init];

// 一般的setter用法
[b setPrice:300];

// 一般的getter用法
NSLog(@"the price of the book is %d", [b price]);

// Objective-C 2.0之後可使用點語法,它會自動呼叫setter並將值傳給它
b.price = 200;

// 這裡則是呼叫getter把值傳回來
NSLog(@"the price of the book is %d", b.price);

[b release];

這樣有比較習慣了嗎? 我相信很多人對Objective-C的方括號的語法很感冒,至於程式碼的可讀性就看個人了,方括號語法看久了也是滿習慣的。不過用點語法的時候要注意幾件事,例如你自己寫了一個setter,又在裡面用了點語法:

1
2
3
4
5
-(void) setPrice: (int) p
{
  // 如果這樣寫的話,會啟動setPrice這個method
  self.price = p;
}

然後..就變無窮迴圈了! 這點要特別注意一下。

還有個問題是其實這個點語法光從程式碼其實看不太出來到底是物件還是結構還是變數..再來看另一個例子:

1
2
3
CGPoint pos = player.position;
pos.x += acceleration.x * 10;
player.position = pos;

這是一段Cocos2D的語法,內容大意是說:「我要建立一個CGPoint變數叫做pos,而這個pos是由player這個CCSprite的position來的」。acceleration是一個UIAcceleration物件,取得acceleration的x的值,修改pos裡的x之後再把整個pos變數塞回去給player這個角色。

這段程式碼對老手來說大概會覺得很弱,覺得這大概是新手,還得要建一個暫存變數,遜! 要是他來寫根本就可以直接併成一行:

1
player.position.x += acceleration.x * 10;

但事實上這樣寫的話會丟出一個”lvalue required as left operand of assignment”的編譯錯誤。為什麼會這樣? 分段來看:

1
player.position.x

其實這種”點語法”是一種”語法糖衣(syntactic sugar)”,事實上它是:

1
[player position].x

這邊position是一個getter,可以讓你從player身上取得一個CGPoint型態的position(r-value),但因為並沒有把指定給某個變數(l-value),所以當你要想用setter把它寫回去的時候,這個值已經消失了。

感想:看來Objective-C不只語法不同,連一些習慣寫法也不同了,眉眉角角真多!

建議閱讀:

OOP in Objective-C

這回來我們來看看在Objective-C裡的物件導向程式(Object-Oriented Programming)的東西,不過有些像是”何謂物件導向”以及”物件導向的優缺點”之類的基礎觀念就不特別提了,網路上應該可以找到更多更詳細的參考資料,我假設你曾經在別的程式語言有寫過OOP,大概知道繼承、封裝是怎麼回事。

在別的程式語言裡,會用關鍵字class來定義類別,在Objective-C的話則是用@interface這個關鍵字來定義。不要搞混這個@interface跟其它程式語言的interface,它們講的是不同的東西(其它程式語言的interface在Objective-C裡比較像是Protocol,之後會再做說明)。為了讓環境單純一點,我們直接開一個Command Line Tool的專案來練習手感:

image

下一步設定專案名稱,我取名為OOPTest。再來我們要來新增個類別,在Source資料夾上按右鍵,新增檔案:

image

選擇一個Objective-C Class,Subclass的地方選擇NSObject:

image

再來因為我要來寫個Book的類別,所以我就把它取名為Book.m,這邊要注意的是,這裡有個”Also create Book.h”也把它勾起來,它會一起幫忙產生header檔:

image

要特別說明的是,在Java/AS3裡,類別名稱跟檔案的名稱(*.java、*.as)是要同名的,但在Objective-C裡卻沒這樣的強制規定,不過為了方便維護起見,我會習慣把類別跟檔名取一樣的名字。另外這邊的”.m”跟”.h”檔,.h檔是它的”h”eader定義檔(放@interface的地方),.m則是它的i”m”plementation實作檔(放@implementation的地方),其實你要放在同一個檔裡,甚至是全部直接寫在main裡也ok,但為了好維護,通常我們會另外把它拆成兩個檔案來寫。接著我們就要來Book.h裡放類別的定義:

1
2
3
4
5
6
7
8
9
10
@interface Book : NSObject
{
  int price;
}

-(int) price;
-(void) setPrice: (int) p;
+(void) printBookInfo;

@end

其實這個類別定義,用翻譯成一般話就是說「我定義了一個叫做Book的類別,在這個類別裡有一個price的整數變數,並寫了一對的getter跟setter,還有一個叫做printBookInfo的class method」。為什麼需要getter跟setter? 在 Objective-C 裡的實體變數(instance variable,可簡稱為ivar)預設是 protected 的,所以你沒辦法從外部直接存取。當然也是可以透過 @public 語法來設定成公開的屬性,但這並不是好的 OOP 實作方法。

這裡有幾個比較陌生的字,NSObject是Objective-C裡最上層的類別,所有的物件都是繼承自它。要繼承的話,只要用一個冒號”:”即可,後面放的是要繼承的父類別名稱。有注意到前面的加號(+)跟減號(-)了嗎? 它也是有特別意義的,加號代表這個方法是屬於類別方法(class method),減號則表示這個方法是實體方法(instance method)。有什麼不同? class method是針對類別本身呼叫的,不需要產生instance即可使用,像這樣:

1
[Book printBookInfo];

而instance method則需要先由類別產生物件,再由物件來呼叫:

1
2
3
Book *book = [[Book alloc] init];
[book setPrice: 20];
NSLog(@"the price of the book is %i", [book price]);

在Java/AS3裡,類別的定義跟實作是寫在一起的,但在Objective-C裡則是分開寫的。到目前為止,我們只用了@interface只是”定義”了類別的骨頭,但還沒實際把肉填進去,要填肉的話,用的是@implementation語法,接著切換到Book.m(你可以用command + option + 上鍵來切換):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import "Book.h"

@implementation Book
-(int) price
{
  return price;
}

-(void) setPrice: (int) p
{
  price = p;
}

+(void) printBookInfo
{
  NSLog(@"Hello, This is a book");
}

@end

記得我們在@interface裡有定義了2個instance method跟1個class method,在@implementation就要把它實作出來。補充一下,這裡的#import跟在c/c++裡出現的#include有類似的用途,都是把東西給匯進來使用,但這兩個語法最大的差別是#import只會匯入一次,所以你可能會在c/c++的程式碼裡常看到#ifdef之類的用法來做檢查,但如果用#import就不用擔心這問題了。

最後回來我們整個程式的進入點,在這個範例裡是OOPTest.m(如果你的專案取名字跟我的不同,這個檔名也會不同)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#import <Foundation/Foundation.h>
#import "Book.h"

int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

  // 新增一個Book實體
  Book *b = [[Book alloc] init];

  // 設定價錢
  [b setPrice:20];

  // 把價錢印出來
  NSLog(@"the price of the book is %i", [b price]);

  // 用完記得放掉
  [b release];

  // 這是類別方法
  [Book printBookInfo];

  [pool drain];
  return 0;
}

按下”Build and Run”之後的執行結果:

the price of the book is 20
Hello, This is a book

如果你的程式在編譯過程有發生錯誤而沒辦法正常的執行,那可能是有程式碼打錯字,再回頭檢查看看,常見的錯誤是大小寫打錯,還有每行程式碼最後面記得要加分號。

你也許會好奇,Book哪裡來的allocinit,還有release方法? 因為這些方法都是繼承來的,不用特別寫就有了。

不過每次為了某個變數就要寫一對getter/setter應該覺得很煩人吧,Objective-C有提供了@property@synthesize語法,之後會再特別說明相關用法。另外,Objective-C是單一繼承的,可以靠Protocol以及Category來補足這部份的不足。下一篇,我們會來看看@property跟@synthesize是怎麼回事。

建議閱讀: