讓大家久等了,Stanford 大學的 iPhone 開發課程筆記第三堂課來了!或許大家有注意到,在第三堂課的課堂上,老師有提到今年的 iTunes U 影片會較去年慢一點釋出,所以一直拖到現在才恢復連載,還請大家見諒!
這次的課程內容包含如何建立自訂的類別、以及一個類別的生命週期以及記憶體管理,和 Objetive-C 中特別的 Property 概念。
建立自訂類別

在 Objetive-C 中,如果要建立自訂的類別,就跟 C++中的模式很類似,需要一個.h 標頭檔先宣告類別的內容(方法和變數),然後搭配.m 檔來實做這些方法的功能。
而誠如第一次筆記中所談的,所有的 Objective-C 物件都繼承成至 NSObject 這個根物件。
#import <Foundation/Foundation.h> @interface Person : NSObject { // 宣告類別所擁有的變數 NSString *name; int age; } // 宣告類別所擁有的方法 - (NSString *)name; - (void)setName:(NSString *)value; - (int)age; - (void)setAge:(int)age; - (BOOL)canLegallyVote; - (void)castBallot; @end
以上的這段程式碼就是一個簡單類別的定義,在@interface 下的括號中包含了這個類別的狀態,也就是他所擁有的變數,而在後面的部份則是方法的定義。
還記得我們曾經在之前的課程筆記中提到,在 Objetive-C 中,由減號 (-) 開頭的方法就是實體方法,必須由類別產生出實體之後才能使用。而由加號 (+) 開頭的方法則是類別方法,不需產生出實體就可以使用。
順便提醒,最後可別忘記在宣告完類別後加入@end 的標籤。
在物件實做的過程,也就是在.m 中實做方法的時候,我們可能會需要傳遞訊息給自己本身的物件,也就是呼叫自己本身帶有的方法,此外,也有可能需要傳遞訊息給父類別、呼叫父類別的方法。而在 Objetive-C 採用了類似 C++和 Java 的模式,用 self 代替自己、用 super 替代父類別。也就是:
- (void)doSomething { // 呼叫父類別的方法 [super doSomething]; // 呼叫同類別中的方法 [self doAnotherThing]; }
物件的生命週期
當我們設計好類別之後,隨之便是要在程式中將這些類別拿來使用,也就是產生實體。而在 Objective-C 中,我們通常會透過 [[anyClass alloc] init]; 這樣的方式來產生新的物件。
+ alloc 這個類別方法就像是 C 語言中的 malloc 或是 C++中的 new 一樣,會在記憶體中產生一個新的物件出來,並回傳該記憶體的位置給變數。
而在我們產生完新的物件之後,我們需要呼叫建構子來幫助我們初始化該物件的內容,也就是- init 這個實體方法以及其他以 initWith…開頭的實體方法。
也因為上述的原因,我們需要在自己的類別中定義 init 以及 initWith…的相關方法。
- (id)init { // 先讓父類別進行初始化 if (self = [super init]) { // 再進行其它初始化的工作 age = 0; name = @“Bob”; } return self; }
上面這種寫法是 Cocoa 中很常見的寫法,先呼叫父類別的初始化方法,在進行自己所需要初始化的動作。
而我們也需要建立一系列不同的初始化方法,來幫助我們用不同的參數初始化物件,向下面這些就是很好的例子:
- (id)init; - (id)initWithName:(NSString *)name; - (id)initWithName:(NSString *)name age:(int)age;
在我們有多重的初始化方法之後,我們實做的時候要記得一個關鍵:只要寫最複雜的那個方法就好,其他的都呼叫那個方法就對了。像是以下就是個例子:
- (id)init { return [self initWithName:@“No Name”]; } - (id)initWithName:(NSString *)name { return [self initWithName:name age:0]; }
除了建立物件之外,學習如何刪除物件也是很重要的,在 Cocoa Touch 的環境下,我們並沒有像 Java 一樣方便的 Garbage Collection 物件自動回收機制可以使用,而必須像 C++一樣,自己去追蹤物件的使用,在適當的時候把物件刪除。
在 Objetive-C 裡面,當物件要從記憶體中刪除的時候,會呼叫-dealloc 這個方法。然而我們並不需要自己去呼叫這個方法,因為 Objetive-C 為了方便大家能夠持續追蹤物件的使用狀況,提供了 reference count 的機制,也就是物件被參考的次數,會被儲存在一個變數中(相關的方法跟變數實際上是繼承至 NSObject 物件)。
而我們可以透過 release 這個方法來減少 count、用 retain 方法來增加 count。當 count 的值降到零的時候,物件就會從記憶體中被釋放出來。
每當物件被 alloc + init 建立的同時,他的 reference count 就會變成 1,隨著物件的操作或傳遞,我們在適時的進行 release 和 retain 的呼叫,這樣當物件的 conut 歸零的同時,物件就會被呼叫 dealloc 方法、進而從記憶體中刪除。
Memory count
在上面我們提到,我們曾經提到在 Objetive-C 裡面必須自己去處理物件在記憶體的建立以及刪除,而為了方便我們解決這個問體,NSObject 提供了 memory count 的機制:當物件產生時這個數字會是 1,透過 retain 可以再增加 1,而 release 會減一,當數字小於等於 0 的時候就會呼叫該物件的 dealloc 方法,將物件從記憶體中刪除。
此外,我們可以透過呼叫物件的 retainCount 方法來檢查目前物件的 memory count。不過需要注意像是下面這種狀況:
Person *person = [[Person alloc] init]; // 建立物件 [person release]; // 物件從記憶體中被移除 [person doSomething]; // 程式錯誤!
因為 person 這個變數仍然存著一個記憶體的位置,系統無從得知 person 所指向的記憶體位置是不是已經被釋放了,所以必須在第三行加入:
person = nil;
誠如在第一次課堂筆記中所說的,Objetive-C 可以接受呼叫一個不存在的方法,因此對於 nil 呼叫任何方法都不會產生錯誤。
物件的所有權
一般來說,如果我們使用了某個 initWith…開頭的方法來初始化物件的話,我們必須記得 retain 這些傳入的參數:
- (void)setName:(NSString *)newName { if (name != newName) { [name release]; name = [newName retain]; // 物件的 memory count 加一 } }
然而,這會面臨一個問題是,傳入的 newName 跟物件自己擁有的 name 兩個其實是在記憶體中指向同一個物件,所以如果在物件中對 name 這個變數做內容的修改,一樣會影響到當初傳入的 newName。所以針對這個問題,Objetive-C 也提供了相對的解決方式::
- (void)setName:(NSString *)newName { if (name != newName) { [name release]; name = [newName copy]; //在記憶體中另外複製了一份 newName,並且 memory count 為 1 } }
這樣的話我們就算修改物件 name 變數的內容,那也並不會影響到本來傳入的參數。
實做 dealloc
當 A 物件如果 retain 了 B 的話,那我們在 A 物件被 dealloc 的時候需要一同 release B 物件,這樣才不會造成 memory count 無法歸零的狀況,以下是一個常見的作法:
- (void)dealloc { // 先處理本身所擁有的物件變數 [name release]; // 當處理完之後就可以呼叫父類別的 dealloc 方法 [super dealloc]; }
autorelease

在我們呼叫物件的方法時,這個方法可能需要會回傳另外一個物件給我們,像是以下的範例:
- (NSString *)getSomething { NSString *result = [[NSString alloc] initWithString: @"Something"]; return result; }
然而,像是以上這樣的範例會造成記憶體上的問題,因為我們在建立了 result 並且回傳之後,就不會在用到這個物件了,但是我們卻沒有 release 這個物件。所以,你可能會想要這樣寫:
- (NSString *)getSomething { NSString *result = [[NSString alloc] initWithString: @"Something"]; [result release]; return result; }
可惜,這樣的寫法也是有問題的,因為回傳的 result 物件再回傳之前就先 release、memory count 歸 0 了,所以再回傳之前就先在記憶體中被刪除了!針對這個問題,Objective-C 有一個特別的機制可以幫助我們,也就是 autorelease。我們之需要將 [result release]; 這行換成 [result autorelease]; 就可以解決這個問題了!
而這樣方便的功能是怎樣完成的呢?或許大家已經注意到,在之前每個作業中的程式碼通常都會以下列的形式呈現:
int main(int argc, char *argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; // do something [pool release]; }
這邊的 NSAutoreleasePool 就是幫助我們完成這個功能的重要角色,他會將所有被最近 autorelese 的物件收集起來,當這個 pool 本身被 release 的時候,他就會把這些物件一一 release 掉。而 UIKit 針對每一個處理的事件會自動的用一層 AutoreleasePool 包起來,所以每當一個 UI 事件處理完成的時候,這些物件都會相繼被 release 掉。
命名慣例
我們都知道了 autorelease 是如此的方便,而在 Cocoa 的 API 中有一些命名習慣就是與記憶體管理息息相關的:
- 所有以 init、copy、new 開頭的方法會回傳物件帶有 memory count= 1,使用者必須在適當的時候 release 該物件
- 而其他所有的方法會直接回傳一個被 autorelease 過的物件,所以使用者必須自己 retain 該物件才能做後續的使用
我會建議各位讀者在自行撰寫方法的時候,也要盡量遵循這些命名慣例,以講求未來使用上的一直性。
@property
在物件導向一般的開發流程中,我們需要針對物件實做很多方法來讓使用者能夠操作物件中的變數。假設我們的物件定義擁有這些方法:
- (NSString *)name; - (void)setName:(NSString *)value; - (int)age; - (void)setAge:(int)age; - (BOOL)canLegallyVote;
但我們可以注意到,這些方法其實都是為了讀取或是操作物件內的變數而所產生的方法。透過@property 的語法,我們可以寫成:
@property int age; @property (copy) NSString *name; @property (readonly) BOOL canLegallyVote;
是不是簡潔很多呢?事實上,如果當我在.h 檔中宣告了:
@property int foo;
其實就等同於我寫了:
- (int)foo; - (void)setFoo:(int)value;
而在.m 檔中,我們也不需要一一實做這些@property 所產生的方法,我們只要寫:
@synthesize foo;
就會等同於產生了:
- (int) foo { return foo; } - (void) setFoo:(int)value { foo = value; }
透過@property 和@synthesize 就可以減少很多這類重複的工作,這樣方便的功能可得牢記在心!
但需要注意的是,@property 並非一定要搭配@@synthesize 使用,你可以只在.h 檔中宣告@property 而在.m 檔中自行實做方法,而不透過系統自動產生。這樣做的原因通常是你需要在修改物件變數內容時做一些檢查,檢查設定的值是否合法。
Property 的屬性
除了單純簡化變數的存取之外,@property 也支援了許多不同的屬性。像是設定了 (readonly) 屬性的@property 只會產生讀取用的方法、不會有設定變數的方法產生。而針對記憶體管理的部份,Obj-C 提供了三種不同的屬性,分別是 (assign)、(retain) 和 (copy)。
一般的@property 在沒有設定屬性的狀況下預設值為 (assign),而 (assign) 屬性會直接傳入的參數物件直接指派給物件的變數,這樣會造成兩個問題。一個是沒有 retain、另外一個是物件變數的內容及傳入的參數其實兩個是指到相同的內容。
因為上面這兩個問題,分別有了 (retain) 和 (copy) 兩個參數來解決,搭配 (retain) 的 property 會在@synthesize 的時候一併對傳入的物件 retain,而 (copy) 則是會對傳入物件另外拷貝一份。
點號和 self
當我們針對某個物件變數設定了@property 屬性之後,我們就可以使用像 foo.name 這樣的方式來取用變數,或是用 foo.name = @"Apple"; 這樣來設定變數。
但是需要注意的是,當我們在實做物件的時候可千萬要分清楚兩者:
@implementation Person - (void)doSomething { name = @“Fred”; // 直接設定物件變數 self.name = @“Fred”; // 呼叫 setName 這個方法 }
錯誤的使用可能會造成無窮迴圈:
-(void) setName: (NSString *)value { self.name = value; // 這樣又會呼叫 setName 這個函式 }
結論
在漫長的基礎訓練之後,我們終於把 Obj-C 中比較困難的部份學完了!也就是我們已經具備了足夠的基礎,接下來就是開發 iPhone App 了!在下一次的連載中,我們將介紹 iPhone 上的介面開發,還請大家繼續指教!