讓大家久等了,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的屬性
除了單純簡化變數的存取之外,@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上的介面開發,還請大家繼續指教!