高見龍

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

iPhone app 實作練習 - 地圖 app

image

有看過在iPhone裡直接秀出地圖app嗎? 看起來挺酷的吧。猜猜看寫一個把google map載入到iphone的功能要花多少時間? 又要寫幾行程式碼? 如果只是要單純的把地圖拉進iPhone顯示,答案是3分鐘以內(還包含開啟Xcode、interface builder跟編譯的時間,如果動作快一點,不用1分鐘就搞定了),而且還不用寫到一行程式碼,你可以親自開Xcode來試試看就知其實並不會太麻煩的。

在iOS 3.0之前要讀取地圖可能得用一些比較麻煩的方法去抓google map進來,但在3.0之後新增了MapKit Framework之後,人生就都變彩色的了。

打開Xcode,新增專案,這裡我先偷懶的選用view-based application,選擇專案名稱,這裡我幫它取名為MapDemo(不一定要取這名稱,只是如果你取的名稱是不同的,後面專案的程式碼檔名可能會有所不同)。

image

專案新增後,打開Resources資料夾裡的MapDemoViewController.xib,它會自動啟動interface builder,接著到Library裡找到一個叫做MapView的元件,把它拖拉到場景上放好對齊。

image

存檔,回到xcode環境。接下來看一下你的Frameworks資料夾,因為預設的Framework只放了UIKit、Foundation跟CoreGraphics這三個,如果要用到MapView的話,還需要把MapKit Framework加進來,不然編譯會發生錯誤。在Frameworks資料夾按右鍵->Add->Existing Frameworks..

image

找到MapKit Framework,把它加進來:

image

你應該就可以在你的Frameworks資料夾裡看到MapKit了:

image

加好之後,執行Build and Run,這樣就完成了!

image

從開始到現在如果手腳快一點可能還不用一分鐘就搞定了,而且除了一開始你打了MapDemo幾個字母的專案名稱外,應該沒有再需要用到鍵盤輸入任何程式碼。MapView預設是以Google Map為資料來源,而且這個地圖已經有包含放大、縮小、拖拉的功能了。你可以在試著模擬器裡按著option鍵加滑鼠移動可以模擬你在實機上縮放的手勢。

但是,就這樣出現個世界地圖的app似乎沒什麼鳥用,跟之前看到某房仲業推出的手機看屋的app的複雜度還差得多,不過但至少知道其實在iPhone上面加入地圖功能不算是非常難做的功能。

接著來加一下可以依據經緯度座標來定位的功能:

1
2
3
4
5
6
7
8
9
10
11
// 檔名:MapDemoViewController.h

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>

@interface MapDemoViewController : UIViewController
{
  IBOutlet MKMapView *map_view;
}

@end

然後到interface builder裡,把這個map_view變數”串”到你剛剛拖到場景上的那個MapView,讓我們在程式碼裡面可以透過這個變數去操作場景上的那個元件。這個動作如果不熟悉的話,可以參考前面那篇BMI app實作的文章。

image

串完之後,接著就要來開始寫code了,為了展示目的,我直接把程式碼寫在viewDidLoad裡面:

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
// 檔名:MapDemoViewController.m

#import "MapDemoViewController.h"

@implementation MapDemoViewController

- (void)viewDidLoad
{
    // 建立一個region,待會要設定給MapView
    MKCoordinateRegion kaos_digital;

    // 設定經緯度
    kaos_digital.center.latitude = 25.01141;
    kaos_digital.center.longitude = 121.42554;

    // 設定縮放比例
    kaos_digital.span.latitudeDelta = 0.007;
    kaos_digital.span.longitudeDelta = 0.007;

    // 把region設定給MapView
    [map_view setRegion:kaos_digital];

    [super viewDidLoad];
}

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

@end

存檔,回到Xcode執行Build and Run,畫面看起來應該比剛剛的好多了。剛我在程式裡設定的那組經緯度是我公司的位置,沒問題的話你看到的地圖應該會在台灣新北市樹林跟新莊的交界處附近。

image

另外,MapView預設的模式是一般的街道地圖的,你可以用程式來調整:

1
2
3
4
5
6
7
8
// 這是預設的標準模式
map_view.mapType = MKMapTypeStandard;

// 衛星地圖模式
map_view.mapType = MKMapTypeSatellite;

// 混合模式
map_view.mapType = MKMapTypeHybrid;

或是在interface builder環境,從MapView的inspector去調整:

image

如果你想要秀出使用者的所在位置:

1
map_view.showsUserLocation = YES;

或是用inspector設定:

image

如果你把這個選項開啟的話,在模擬器裡會出現在美國的某個有趣的地方:

image

希望做到目前這個階段對大家還不會太困難,但老實說就算現在這樣其實也沒什麼用,甚至也不知道到底指定的位置在哪裡,想靠它賺大錢還早。不過沒關係下一篇我們會來弄一些比較進階的功能,例如是幫地圖加上大頭針,或是加一些自訂的icon圖示等功能,讓地圖看起來更漂亮、實用些。但因為可能會用到在Objective-C裡的Protocol觀念,所以在正式開始實作之前,會先來說明一下在Objective-C裡的CategoryProtocol是怎麼回事。

以上,希望對各位有幫助,若有錯誤的地方還請不吝指教,感謝 :)

Objective-C記憶體管理之Autorelease

上一篇概略的提到了Objective-C裡的記憶體管理,其實Objective-C其實還滿多這種眉眉角角的地方要注意的,接下來我們來看看關於Autorelease Pool的東西。這篇文章會比較長,而且會比較悶又囉嗦,還請多多擔待。

在開始之前要先看一下這一行程式碼:

1
NSMutableString *s1 = [NSMutableString stringWithString:@"hello eddie"];

如果用地球的語言來說,就是「建立一個NSMutableString型態的s1指標變數,這個指標變數指向某個內容為”hello eddie”的NSMutableString物件」,在s1前面的那個星號*表示這個s1是一個指標(pointer)變數,不過這裡不會對指標有太多的說明(因為我跟指標也不熟),有興趣可再找找C/C++的相關書籍來研究。在Objective-C裡的物件變數都是指標,所以來看看底下這個例子:

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
#import <Foundation/Foundation.h>
int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

  // 建立2個字串變數
  NSMutableString *s1 = [NSMutableString stringWithString:@"hello eddie"];
  NSMutableString *s2 = s1;

  // 印出這兩個字串的位置
  NSLog(@"location of s1 is %p", s1);
  NSLog(@"location of s2 is %p", s2);

  // 印出兩個字串的內容
  NSLog(@"s1 is %@", s1);
  NSLog(@"s2 is %@", s2);

  // 改變了s1的內容
  [s1 setString:@"eddie"];

  // 看看這兩個字串的內容是否改變
  NSLog(@"s1 is %@", s1);
  NSLog(@"s2 is %@", s2);

  [pool drain];
  return 0;
}

輸出結果:

location of s1 is 0x10010cad0
location of s2 is 0x10010cad0
s1 is hello eddie
s2 is hello eddie
s1 is eddie
s2 is eddie

因為s1跟s2其實是指向同一個地方,所以印出來的記憶體位置也是相同的。一但修改了s1的內容,指向同一個地方的s2也會跟著改變。為什麼要特別提這個? 待會後面會用到。

回到主題,在上一篇文章有提到東西用完要release掉,把記憶體還給系統,這個autorelease pool照字面看起來像是會自動釋放資源的東西,是沒錯,但其實它並不是Garbage Collection。簡單的說,想像你有個游泳池(pool),裡裝著一堆被標記為autorelease的客人,當這個池子裡的水要放掉的時候,它會自動對這個池子裡的所有客人發送release訊息叫他們滾蛋,上一篇也提到,當這些客人的retain count降到0的時候,dealloc方法會自動被呼叫,然後把自己清掉並歸還記憶體。

有點複雜嗎? 我直接借用Stanford大學CS193P第三堂課的其中一張投影片來說明可能會比較清楚:

image

這個圖示就是app運作的生命週期,前半段的程式啟動、載入..等等的動作反正就是那麼一回事,重點在於wait for event跟handle event的這個迴圈(run loop),如果沒意外,這個迴圈會一直執行下去,直到可能有人打電話來中斷了你的app,或是你按了home按鈕離開這個app為止。從圖例裡可以看到而autorelease pool在每個run loop都會進行釋放。

那要怎麼樣把物件設定為autorelease? 兩個方法:

1.明確的呼叫autorelease方法

1
NSNumber *n1 = [[[NSNumber alloc] initWithInt:100] autorelease];

2.上一篇有提到「當你用alloc、new或是copy開頭的方法建立一個物件的話,程式就會向系統要一塊記憶體來放這顆物件,而這顆物件就算在你頭上」,其實除了這些以外的方法來建立物件件的話,它回傳的就是個autorelease物件了。舉例來說:

1
2
3
4
// 你用alloc建立的,用完就要把它release掉
NSNumber *n1 = [[NSNumber alloc] initWithInt:100];
// 用完之後release
[n1 release];

如果是這樣:

1
NSNumber *n1 = [NSNumber numberWithInt:100];

這樣不需要再對n1呼叫autorelease方法,這個n1就是autorelease了。

要特別注意的是,autorelease跟release不同,release會馬上在執行後把retain count減1,而autorelease則是待會才會減1。另外,在上一篇最後提到的記憶體管理原則:「You only release or autorelease objects you own」,一但你把物件設定為autorelease之後,這個東西基本上就不算是你管的,你之後就不需要也不應該再去對它做release的動作了。

或許你會覺得用一般的release就好了嗎? 為什麼特別需要用到這個autorelease? 其實有些地方還是真的非它不可,我們來看看底下這個例子:

1
2
3
4
5
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  return the_name;
}

這裡我們寫了一個會回傳NSString物件的方法,看起來好像沒問題,而且在其它語言也常這樣寫。但注意到在這邊這個區域變數the_name是用alloc產生的,但都沒地方把它release掉,所以可能會出問題。好吧,即然這樣那就把它release掉,所以改寫成:

1
2
3
4
5
6
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  [the_name release];
  return the_name;
}

這樣不行,因為在return之前就release掉了。那把release動作放到return後面?

1
2
3
4
5
6
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  return the_name;
  [the_name release];
}

這當然也不行,在return之後的動作是不會執行的。這時候,autorelease就派上用場了:

1
2
3
4
5
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  return [the_name autorelease];
}

這樣這個區域變數因為被標記成autorelease,它在待會,也就是run loop結束的時候會自動被釋放掉了。

再舉個例子,在Objective-C裡,物件的instance variable預設是設定為protected的,如果沒有getter/setter的話是沒辦法取得取得或設定該物件的屬性(如果你是用@property/@synthesis的做法就不用這麼麻煩了)。所以,舉個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface Book : NSObject
{
  NSString *title;
}

-(void) setTitle: (NSString *)book_title;
-(NSString *)title;
@end

@implementation Book
-(void) setTitle: (NSString *)book_title
{
  title = book_title;
}

-(NSString *)title
{
  return title;
}
@end

我在這裡寫了一組getter跟setter,分別可以設定及讀取Book這個類別的title屬性,看起來是很直覺的getter/setter寫法,在其它的程式語言中也常這樣寫。其實在有GC的環境,這樣寫是沒問題的[註1],但在不支援GC的環境上,例如iphone,這樣的寫法可能會出問題,直接來看個例子:

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
int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

  // 建立物件
  Book *book = [[Book alloc] init];
  NSMutableString *s1 = [NSMutableString stringWithString:@"hello eddie"];

  // 把字串s1透過setter設定給title屬性
  [book setTitle:s1];

  // 輸出結果:the title of the book is 'hello eddie'
  NSLog(@"the title of the book is '%@'", [book title]);

  // 修改s1的內容
  [s1 setString:@"not eddie"];

  // 輸出結果:the title of the book is 'not eddie'
  NSLog(@"the title of the book is '%@'", [book title]);

  // 把s1釋放掉
  [s1 release];

  // 這一行就爆炸了..
  NSLog(@"the title of the book is '%@'", [book title]);

  [book release];
  [pool drain];
  return 0;
}

記得一開始有提到指標的東西,在這裡當把s1設定給title的時候,其實是s1跟title是指向同一個地方,所以當修改s1,title也會跟著改,這不一定是你想要的結果。更麻煩的是,當你送release訊息給s1之後,title也跟著指不到地方。所以setter的地方應該改寫成:

1
2
3
4
5
6
7
8
-(void) setTitle: (NSString *)book_title
{
  if (title != book_title)
  {
      [title release];
      title = [book_title retain]; // 或是copy,端看用途而定
  }
}

為什麼這裡要先做判斷是否相同? 因為title跟傳進來的book_title有可能會指向同一個物件,如果直接先把title release掉,會導致book_title一起跟著無法存取。如果title跟傳進來的book_title的記憶體位置不同的話,那就把原來的title放掉,並從把傳進來的book_title做retain(或copy),確保retain count增加,即使像剛剛那個情況把傳進來的東西release掉,因為retain count不會降到0,所以就還會留著。這裡也可以使用autorelease來處理:

1
2
3
4
5
-(void) setTitle: (NSString *)book_title
{
  [title autorelease];
  title = [book_title retain]; // or copy
}

這邊就不做判斷了,直接把title設定成autorelease,然後待會交給autorelease pool去放掉就好。

這樣看來,感覺autorelease好像很方便,何不乾脆把全部都交給autorelease pool處理好了? 的確是比較方便沒錯,但如果可以的話,請儘量用手動的release而不要用自動的autorelease。一來程式設計師能在資源用完之後馬上主動還回去本就是個好習慣,二來autorelease是在每個run loop才釋放一次,雖然每個run loop可能是零點零零幾秒而已,但也有可能在一個run loop裡的某個迴圈裡一下子產生太多的物件,還沒跑到pool釋放的地方就爆炸了(通常遇到這種情況會在產生大量物件的地方放個巢狀的pool在裡面,讓內層的pool提早釋放,而不是等到最外層的run loop才釋放)。

希望這篇文章對一起學習的朋友會有幫助,若內容有誤也請不吝指正。

建議閱讀:

註1:GC支援功能是可以手動打開,不過要記得不是每個環境都有GC可以用的,在Project Settings裡..

image

找到Objective-C Garbage Collection的選項,設定成Supported:

image

iPhone App實作練習-BMI

看一堆Objective-C原理的書,什麼物件、類別、記憶體管理的,沒實作個東西出來就整個沒成就感。突然間想起自己是北醫營養系畢業的,但總是不務正業,終於有機會發揮我專長的時候了,這次就來寫個計算BMI(Body Mass Index)的iPhone app吧。

我的SDK版本是4.1的,所以如果你的SDK版本跟我的不一樣的話,看到的畫面可能會跟我的不太一樣。

開啟Xcode,選擇iOS Application->View-Base Application:

image

下一步,選擇專案名稱,我這裡取名為「BMI」,存檔,準備開始動工。接下來,你應該會看到一堆資料夾跟檔案(如下圖):

image

暫時先不解釋那些檔案要幹嘛的,直接按下上方工具列的”Build and Run”按鈕。沒意外的話,你應該會看到一個iPhone模擬器自動開起來,並且看到模擬器裡出現一個空白畫面,上面什麼都沒有,因為我們還沒開始寫 :)

接下來要來上面放一些元件了。請點開Resources裡的BMIViewController.xib (如果你的專案名稱跟我不同的話,檔名也會跟著不同)。點開之後應該會開啟Interface Builder,這個是讓你用視覺的方式把元件佈置到場景上的工具。接下來你應該可以找到一個library視窗(如下圖),裡面放了許多預先建立好的元件。如果找不到這個視窗的話,請至上方工具列裡選擇”Tools”->”Library”。

image

接下來我們會從元件庫裡拿幾個元件出來用:

  • Label 是拿來放一些靜態文字用的,提示使用者某個欄位要輸入什麼值,在這裡也可以拿來做為BMI結果的輸出值。
  • Text Field 是用來讓使用者輸入身高體重的地方。
  • Button 則是按下去之後會幫我們計算BMI,並呈現在畫面上。

接下來請把畫面佈置得如下圖:

image

label或是button上的文字,只要點兩下就可以進行編輯。在畫面中間下面,你看到的那個Label字樣,是待會計算完成會出現的結果。但在按下計算之前,它應該是隱藏的,所以點選Label之後,可以從Attributes Inspector把它設定為”hidden”(如下圖)。這樣的話到時候它出場的時候就會看不見了(但在設計階段會看到半透明的)。

image

在attributes inspector這邊還有很多屬性是可以自己調整看看的,例如字型、顏色、大小等。

完成之後記得存檔,然後回到xcode,再度按下”Build and Run”,你就可以看到你剛剛做的成果,按鈕可以按了,文字可以輸入了,但應該都還沒功能,而且那個鍵盤會一直卡在那邊,不會自己退下去,接下來我們就要來把這個功能給完成。

回到Xcode,點開左邊Classes裡的BMIViewController.h(同上,如果專案名稱不同,檔名也會不同),你會看到一個空的類別定義,這裡暫時不詳細說明什麼是類別,先讓大家取得成就感再來研究細節 :)

接下來我們要在@interface裡加幾個屬性跟方法,程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 檔案:BMIViewController.h
#import <UIKit/UIKit.h>

@interface BMIViewController : UIViewController
{
  IBOutlet UITextField *body_height;
  IBOutlet UITextField *body_weight;
  IBOutlet UILabel *result;
}

@property (nonatomic, retain) IBOutlet UITextField *body_height;
@property (nonatomic, retain) IBOutlet UITextField *body_weight;
@property (nonatomic, retain) IBOutlet UILabel *result;

-(IBAction) calcBMI: (id) sender;
-(IBAction) keyboardDismiss: (id) sender;
@end

這裡先暫時不說明什麼是IBOutlet、IBAction還有一堆看不懂的語法是要幹嘛的,有機會的話,會在之後的文章裡再來一一說明。簡單的說,這些IBOutlet就是一些待會要給Interface Builder做連結用的,IBAction則是一些”動作”,待會要設定給按鈕按下去之後用的。這裡的IB,指的就是Interface Builder

定義好了interface之後,接下來請切換到BMIViewController.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 檔案:BMIViewController.m
#import "BMIViewController.h"

@implementation BMIViewController

@synthesize body_height;
@synthesize body_weight;
@synthesize result;

-(IBAction) calcBMI: (id) sender
{
  // 取得身高數值,並換算成公尺
  float height = [[body_height text] floatValue] / 100;

  // 取得體重數值
  float weight = [[body_weight text] floatValue];

  // 顯示計算結果
  result.hidden = NO;
  result.text = [NSString stringWithFormat:@"您的BMI值是:%.2f", (weight / (height * height))];
}

// 把鍵盤退下去
-(IBAction) keyboardDismiss: (id) sender
{
  [sender resignFirstResponder];
}

-(void) viewDidLoad
{
  // 設定這兩個欄位的初始為數字及標點符號鍵盤
  [body_height setKeyboardType:UIKeyboardTypeNumbersAndPunctuation];
  [body_weight setKeyboardType:UIKeyboardTypeNumbersAndPunctuation];
}

- (void)dealloc
{
  [body_height release];
  [body_weight release];
  [result release];
  [super dealloc];
}
@end

特別要說明的是,calcBMI方法做的事,就是去取得畫面上的兩個TextFeid的值,計算之後把結果輸出到剛剛那個被我們隱藏起來的Label;keyboardDismiss方法則是要把鍵盤給退下去,而viewDidLoad則是當整個app執行並載入元件完成後會做的事,這裡我們設定那兩個TextField的keyboard屬性為數字+標點符號鍵盤(身高體重應該只要輸入數字跟小數點)。

到這裡,程式碼的部份其實已經算寫完了,但是還沒”串起來”,所以還不會動。接下來我們就是要來做”串起來”的動作。

回到Interface Builder,找到File’s Owner,在上面點滑鼠右鍵應該會出現像下面這個畫面:

image

你剛剛寫的程式碼如果沒錯的話,IBOutlet跟IBAction就會在這裡出現了。接下來就是開始歡樂的連連看了。上圖的IBOutlet跟IBAction旁邊都有一個小圈圈,按下body_height屬性右邊的小圈圈,然後拖拉到你剛佈置的那個身體的TextField,如下圖:

image

body_weight也是以此類推,再來把calcBMI這個IBAction拖拉到計算的按鈕上,它會出現一個選單,選擇”Touch Down”,表示這個IBAction會在這個按鈕被按下去的時候執行:

image

同樣的把result也拖拉到那個隱藏的label上:

image

最後比較特別的是keyboardDismiss這個,請把它各別拖拉到身高及體重的TextField,並選擇”Did End On Exit”,這表示當在這兩個TextField按下enter或return之後會做的事,在這裡是把鍵盤給退下去:

image

如果全部都完成的話,畫面應該看起來會像這樣:

image

以上這些”串連”的動作,就是把畫面上的東西跟程式碼串接起來,讓按鈕按下去會有反應,該出現的出現,該隱藏的隱藏。除了從File’s Owner這邊可以拉之外,也可以從另外的Connection視窗來看,例如你點一下那顆計算的按鈕,並切換到Connection視窗(如下圖),如果看到這顆按鈕目前有跟什麼屬性或動作有連結。在這裡可以看到這個按鈕的Touch Down事件會去觸發calcBMI動作。

image

存檔,並回到Xcode環境,按下Build and Run,程式沒錯的話,應該就可以看到你的app啟動了。

image

最後,當你按下模擬器裡的home按鈕,你看到你的app的icon是白白的(如下圖):

image

這樣很醜,應該不會有人想買你的app,所以通常都會設計個漂亮的icon來放上去。製作方法也很容易,icon的尺寸是57×57,開影像處理軟體做好之後,回到Xcode,把做好的圖檔拖拉到Xcode裡的Resources資料夾。它會跳一個視窗出來:

image

勾選Copy選項,它就會把你剛做的icon圖檔也複製一份到你的專案裡。接著同樣在Resources資料夾裡有個BMI-info.plist檔案(檔名同專案名稱,所以你可能跟我的不一定會一樣),打開之後中間有個icon file的選項,填入你剛剛做好的icon檔名(我的是eddie_icon.png)。

image

存檔,Build and Run,應該就能看到新的icon了:

image

差不多就這樣了。不過其實還有很多防呆裝置沒做,例如欄位沒填,或是數字除以零..等等,還有畫面也都還沒美化過,所以如果你要拿上架去賣錢的話,記得把這些功能補齊。

好啦,這樣你可以準備上架去狠狠賺它個幾百萬了,有賺錢的話記得分紅啊 :)

原始檔下載

補充:

練習久了,你應該會發現在你的模擬器裡塞了一堆的app,如果想要清除的話,請到/Users/個人帳號/Library/Application Support/iPhone Simulator這個資料夾,把裡面的檔案及資料夾砍掉就行了。

Objective-C的記憶體管理

Objective-C的記憶體管理是個重要的主題,把最近啃書的一些心得寫下來讓自己來加深自己的印象,也順便把觀念整理一下。

之前工作上寫習慣PHP/Python/Ruby/AS3這種比較接近人類腦袋的程式語言,很少有機會去碰到pointer(指標)或是記憶體管理的東西。為什麼變數宣告就宣告,為什麼前面要加個星號(*)? 又有的又不用加? 最近在看Objective-C的書就會覺得很不習慣(不過其實看久了也會慢慢習慣就是了)。

在Objective-C裡的記憶體管理主要有兩種,一個是比較常聽到的Garbage Collection(以下縮寫為GC),另一個則是Reference Counting(以下縮寫為RC)。以車子來舉例的話,你可以先暫時把這兩種記憶體管理方式想像成GC是自排車,RC是手排車,一個會自動幫忙回收沒要用的記憶體,一個則是自己得手動釋放用過不要再用的記憶體。

RC的運作機制是當某個物件生成並初始化之後,它的初始retain count會設定成1(其實不一定,也是有例外)。執行該物件的”retain”方法會讓該物件的retain count加1,而release方法會讓retain count減1。當該物件的retain count降到0的時候,這個物件自動會呼叫dealloc方法把自己解決掉,然後把佔用的記憶體還回來。

也許你會覺得,都什麼年代了為什麼還得程式設計師自己用手動的方式來回收記憶體? 換個角度想,程式設計師為自己寫的程式負責也是件好事,另外,也是最主要的原因就是有些環境就是根本不支援GC機制,所以只好用RC來處理。我相信想學Objective-C很多人都是為了想要開發iPhone App而來的,而iPhone正是那個不支援GC環境的其中之一。

又或許你會覺得這樣一顆小物件是能佔多少記憶體。這種東西積沙成塔的,你借了記憶體來用卻沒還回去,久了可能就會造成”漏水”(memory leaking)的情況。為了避免App在執行的過程中莫名奇妙的地方當掉,只好乖乖的來了解一下關於記憶體管理的機制。

Apple官方的開發手冊裡,光是記憶體管理的部份就有五十幾頁的PDF,建議大家抓回來看一下。官方手冊裡開頭提到關於記憶體管理的大原則:

You only release or autorelease objects you own.

什麼是你擁用的物件? 當你用allocnew或是copy開頭的方法建立一個物件的話,程式就會向系統要一塊記憶體來放這顆物件,而這顆物件就算在你頭上。另外當你用對某個物件使用retain方法之後,那顆物件也算是你要負責的;一個物件可以同時有好幾個主人,而當那個物件你不要用的時候,則使用release或是autorelease方法來把這個擁有的關係給斷絕掉,準備把物件清掉並把佔用的記憶體還給系統。直接來看段範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  NSNumber *n = [[NSNumber alloc] initWithInt:100];

  NSLog(@"the retain count is %d", [n retainCount]);

  [n retain];

  NSLog(@"the retain count is %d", [n retainCount]);

  [n release];

  NSLog(@"the retain count is %d", [n retainCount]);

  [n release];

  [pool drain];
  return 0;
}

輸出結果:

the retain count is 1
the retain count is 2
the retain count is 1

除了retain/release之外,如果被collections,例如array、dictionary或set等等給拉進去的話,它的retain count也會加1;相對的,從Collection裡拿出來的話,它的retain count會減1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  NSMutableArray *array = [[NSMutableArray alloc] init];
  NSNumber *n = [[NSNumber alloc] initWithInt:100];

  NSLog(@"the retain count of n is %d", [n retainCount]);

  [array addObject:n];

  NSLog(@"the retain count of n is %d", [n retainCount]);

  [n release];

  NSLog(@"the retain count of n is %d", [n retainCount]);

  [array release];

  [pool drain];
  return 0;
}

輸出結果:

the retain count of n is 1
the retain count of n is 2
the retain count of n is 1

在這個例子裡可以看到,當使用addObject方法把n放進array之後,它的retain count會加1;當使用removeObjectAtIndex把物件從array移出來的時候,它的retain count會減1。最後,當array本身收到release的時候,它會對目前全部的內容物發送release訊息。所以在上面的例子來說,如果在[array release]之後再想存取n變數,就會出現錯誤訊息。

記得,你retain了一個物件,確定沒要再用之後就release掉。retain跟release的次數通常是成對的,你手動retain了幾次,到時候就得手動release幾次。

有一些常見可能會發生問題的寫法:

1
2
3
4
5
- (void)reset
{
  NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
  [self setCount:zero];
}

這裡用alloc建立物件,卻沒有對等的release,可能造成memory leak。

1
2
3
4
5
6
- (void)reset
{
  NSNumber *zero = [NSNumber numberWithInteger:0];
  [self setCount:zero];
  [zero release];
}

在這裡numberWithIngeter產生的是一個autorelease物件,所以這並不屬於你擁有的,當你對它執行release它可能就會因為retain count變成0而被清掉,在autorelease pool裡因為會再對所有標記為autorelease的物件再送一次release訊息,這時候就會出錯了。請記憶體釋放的原則:「You only release or autorelease objects you own.」,如果物件不屬於你,就不要隨便對它執行release方法

關於Autorelease pool,你可能會在一些剛建立好的專案裡看到它幫寫好幾行程式碼了,autorelease pool並不是真正的GC機制,它比較像是GC的替代品。簡單的說,當你對物件執行autorelease方法時,就是把該物件標記成”待會再釋放”的物件,在每個run loop或是pool drain的時候就會對這些有標記的所有物件發送release訊息。雖說是替代品,但有些地方還是非用不可。

結論:

  1. 在你跟系統要了一塊記憶體來用的當下,請先養成「我什麼時候會還回去」的習慣。
  2. retain/release通常都是成對的,手動做了N次的retain,就記得要做N次的release。
  3. 如果你要某個物件,就retain它;如果不用了,就release它,把記憶體還回去。

之後會再來說明一些關於Autorelease pool細節 :)

新手上路,若有錯誤還請不吝指教

建議閱讀:

retainCount of NSNumber

在Objective-C裡有一種記憶體管理機制叫做Reference Counting,是說當某個物件生成並初始化之後,它的retain count會設定成1。執行該物件的”retain”方法會讓該物件的retain count加1,而release方法會讓retain count減1。當該物件的retain count變成0的時候,這個物件自動會呼叫dealloc方法,然後把記憶體還回來,Objective-C的書上或是官方手冊裡差不多都是這樣教的。

所以,下面的程式碼,2個物件的retain count預期應該會是1:

1
2
3
4
5
NSNumber *n1 = [[NSNumber alloc] initWithInt: 10];
NSNumber *n2 = [[NSNumber alloc] initWithInt: 100];

NSLog(@"retain count of n1 is %i", [n1 retainCount]);
NSLog(@"retain count of n2 is %i", [n2 retainCount]);

輸出結果是:

retain count of n1 is 2
retain count of n2 is 1

n2的retain count是1沒問題,但n1的卻是2,這結果跟我想像的不太一樣,這令我這個初學者很困惑..我知道retainCount不應該被拿來做為流程裡的邏輯判斷的依據,不過我很好奇為什麼會有這樣的差異?

翻了一下網路上的討論,原來是說因為某些數字太常被用到,為了做一些最佳化,所以在系統裡直接就預先產生了一份,再試了一下以下的程式碼:

1
2
3
4
5
6
7
8
9
10
11
NSNumber *n1 = [[NSNumber alloc] initWithInt: 10];
NSLog(@"retain count of n1 is %i", [n1 retainCount]);

NSNumber *n2 = [[NSNumber alloc] initWithInt: 100];
NSLog(@"retain count of n2 is %i", [n2 retainCount]);

NSNumber *n3 = [[NSNumber alloc] initWithInt: 100];
NSLog(@"retain count of n3 is %i", [n3 retainCount]);

NSNumber *n4 = [[NSNumber alloc] initWithInt: 10];
NSLog(@"retain count of n4 is %i", [n4 retainCount]);

結果是:

retain count of n1 is 2
retain count of n2 is 1
retain count of n3 is 1
retain count of n4 is 3

又試著把他們的位址印出來:

1
2
3
4
5
// address of nums
NSLog(@"address of n1 is %p", n1);
NSLog(@"address of n2 is %p", n2);
NSLog(@"address of n3 is %p", n3);
NSLog(@"address of n4 is %p", n4);

結果是:

address of n1 is 0x100108e20
address of n2 is 0x10010cb30
address of n3 is 0x10010cb50
address of n4 is 0x100108e20

這樣似乎就能解釋retain count跟預期不太一樣的原因了,可以看到n1跟n4是指向同一塊記憶體位置,而n2跟n3是不同的位置,retain count都是1,表示n2跟n3在生成的時候是建立一個新的數字,而n1跟n4則是指向一個像是”預先產生而且共享”的數字,所以n1一開始的 retain count是2,n4因為指向跟n1同一個地方,所以retain count變3。而這個最佳化(預先產生)的範圍,似乎是從-1 ~ 12之間。

果然還有很多要學的 :)

新手上路,若有錯誤還請不吝指教!