2015年11月25日 星期三

[iOS] kkbox-ios-dev note Data Source與Delegate的差別?

我們現在可以來看看UITablewView與UItableViewController是怎麼運作的。
UITableViewController在loadView中建立了一個UITableView的instance,
指定成是自已的view,同時將這個view的delegate與data source設定成自已。
一個class可以根據需要,將delegate拆成好幾個,以UITableView來說,
跟表格中有什麼資料有關的,就放在data source中,其餘的method就放在delegate中。

我們在Mac OX X會用到的最龐大的UI元件,莫過於WebView,雖然在iOS上UIWebView被閹割到只剩下四個delegate method,但是Mac OS X上足足有五大類delegate method,網頁頁框的載入進度、個別圖片檔案的載入進度、下載檔案的UI呈現,該不該開視窗或新分頁、沒有安裝Java或是Flash要怎麼呈現、用JavaScript跳出alert該怎麼呈現...都是一堆delegate  method。

假如先不管UITableView怎麼重複使用UITableViewCell的機制(這個機制還挺複雜),
我們要更新UITableView的資料時,先指定data source物件後,要呼叫一次reloadData。
reloadData可能是這樣寫的:
- (void)reloadData {
    NSInteger sections = 1;
    if([dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
        sections = [dataSource numberOfSectionsInTableView:self];
    }

    for (NSInteger section = 0; section < sections; section++) {
        NSInteger rows = [dataSource tableView:self numberOfRowsInSection:section];
        for (NSInteger row = 0; row < rows; row++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
            UITableViewCell *cell = [dataSource tableView:Self cellForRowAtIndexPath:indexPath];
        }
    }
}
我們注意到幾件事情:首先,因為numberOfSectionsInTableView:被定義成optional的
delegate method, delegate不見得要實作,所以我們會用respondsToSelector:
檢查是否有實作。我們可以在protocol的宣告中,指定某個delegate method是required或是optional,如果不特別指定的話,預設都是required。我們簡單看一下
UITableViewDataSource就知道如何定義required與optional的delegate method。

@protocol UITableViewDataSource <NSObject>
@required
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableVIew cellForRowAtIndexPath:(NSIndexPath *)indexPath;

@optional
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;

@end

另外,就是定義在data source的method,是在reloadData中被呼叫,因此我們可以知道 UITableView的data source與delegate的最大差別:我們絕對不可以在data source定義的method中呼叫reloadData,不然就會進入無窮廻圈!

Formal Protocol與Informal Protocol
@protocol這個關鍵字是在Objective-C2.0之後出現的,在這之前要定義protocol,則是寫成NSObject的category,前者叫做formal protocol,後者則稱為informal protocol。
UIKit問世時就採用Objective-C2.0的語法,至於Max OS X,蘋果在2008年開始大幅改寫
Foundation與AppKit,現在絕大多數可以看到的protocol都是formal protocol,但如果
你在maintain一份稍微有點歷史的程式,或是在蘋果少數的API中,還是可以看到informal protocol。在Core Animation裡頭,就可以看到CALayerDelegate、CALayoutManager、CAAnimationDelegate,都還是informal protocol。其中CALayerDelegate、CALayoutManager兩者之間還夾著CAAction這個formal protocol。在兩個informal protocol中間夾著一個formal protocol,實在讓人很反感,為什麼不一起改掉呢?至於CAAnimationDelegate也很怪異:CAAnimation的delegate不是用assign,而是會retain起來。

無所不在的Delegate
由於在Objective-C語言中,delegate相當於event handler的用途,所以,當你在其他平台中看到event handler用得多頻繁,就等於delegate用得多頻繁。
舉例來說:
* 在使用NSURLConnection抓取網路上的資料的時候,無論收到了HTTP response code、是否連線失敗,是否連線結束...都是透過delegate回傳。
* 在使用Core Location的時候,如果CLLocationManager找到了我們的所在位置,或是發現我們正在移動,也都會透過delegate通知。
* 當我們要使用手機拍照、傳送簡訊或是電子郵件等等,當照片拍完,會用delegate回傳image物件,簡訊或是電子郵件傳送成功,也會用delegate告訴我們執行完畢。

甚至當我們在寫一個iOS程式的第一步,其實都是在實作一個delegate mehtod。
我們在Xcode裡頭開了一個新專案之後,下一步往往是實作
application:didFinishLaunchingWithOptions:這個method,但是要了解整個程式的進入點,我們要從main.m來看。裡頭通常只有簡短幾行:
int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
一個iOS程式是從main這個function開始,接著透過呼叫UIApplicationMain建立UIApplication這個Singleton物件UIApplication用來代表一個應用程式的基本狀態,包括icon上面該顯示多少push notification的數量、支援水平還是郵直畫面、是否顯示狀態列等,當UIApplication物件被建立起來後,就要通知它的delegate,程式已經開啟了,進行下一步,這個delegate method就是application:didFinishLaunchingWithOptions:,我們在這邊建立基本的view controller與window,顯示出來。
也就是說,當我們在開始寫第一行iOS程式的時候,我們就起碼需要了解什麼是Singleton和delegate,但是在了解之後,想要知道Mac OS X與iOS中眾多的元件如何使用,以及怎樣用比較好的方式設計自已的元件,就不是問題了。

其他平台上所謂的Delegate
在其他平台中,也用到了delegate這個詞,但是意義不太一樣。
Design Pattern中所講的Delegate
就我的理解,Design Pattern中所講的Delegate Pattern,比較像是做一個Wrapper,
有一個class在實作method時,其實是直接把這個method的實作傳遞到自已的成員變數物件的實作上。以Objective-C語言實作會像這樣:
首先產生一個內部的物件,叫做MyInnerClass:

@interface MyInnerClass : NSObject
- (void)doSomething;

@end

@implementation MyInnerClass

- (void)doSomething {
    NSLog(@"Do something");
}

@end
然後MyClass會把該做的事情,都交給MyInnerClass:

@interface MyClass : NSObject {
    MyInnerClass *innerObject;
}

- (void)doSomething;

@end

@implementation MyClass

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

- (id)init {
    self = [super init];
    if (self) {
        innerObject = [[MyInnerClass alloc] init];
    }

    return self;
}

- (void)doSomething {
    [innerObject doSomething];
}

@end
在Cocoa Framework中,會比較像是NSButton與NSButtonCell的關係。你或許會問,為什麼Objective-C裡頭的delegate與Design Pattern裡頭講的Delegate Pattern意義不一樣呢?為什麼Objective-C不按照這套用法?但其實是,Objective-C使用delegate這個觀念,早於Design Pattern成書。

C#中所謂的Delegate
C#語言中也有delegate這個關鍵字、不過用途卻是處理anonymous function,以我們上面的例子,我們打C#增加被點選的event handler,原本這麼寫:

private void InitializeComponent() {
    this.button1 = new System.Windows.Forms.Button();
    this.button1.Click += Button1_Click;
}

private void Button1_Click(object sender, System.EventArgs e) {

}

在C#2.0可以寫成這樣:
private void InitializeComponent() {
    this.button1 = new System.Windows.Forms.Button();
    this.button1.Click += delegate(object sender, System.EventArgs e) {
        // Do something here.
    }
}

關於在Objective-C語言中怎麼使用anonymous function,我們會在接下來的章節,講block的時候討論。

2015年11月18日 星期三

[iOS] kkbox-ios-dev note 設計Protocol與實作Delegate的方式

我們來用delegate的想法來實作前面提到的狀況。 宣告Protocol與Delegate的方式 我們先來建立一個NSButton的cubclass叫做MyButton,MyButton的delegate必須實作 MyButtonDelegate這個protocol。.h檔案中宣告如下:
#import 

@class MyButton;

@protocol MyButtonDelegate

- (void)myButtonDidBecomeClicked:(MyButton *)button;
- (void)myButtonDidBecomeDoubleClicked:(MyButton *)button;
- (void)myButtonDidBecomeTripleClicked:(MyButton *)button;
- (void)myButtonDidBecomeQuadrupleClicker:(MyButton *)button;

@end

@interface MyButton : NSButton
{
    id delegate;
}

@protocol (weak, nonatomic) id  delegate;

@end
逐行解釋這個header裡頭的內容:
1. #import<Cocoa/Cocoa.h>:因為NSButton是在AppKit裡頭,
所以我們必須呼叫對應的header
2. @class MyButton; :因為在我們接下來的protocol宣告中,會用到MyButton
這個class,但是MyButton其實是宣告在MyButtonDelegate的下面,所以我們需要
先預先宣告MyButton這個class的存在。
3.從@protocol MyButtonDelegate開始,就是在宣告MyButtonDelegate
這個protocol裡頭的四個method。
4. 接下來宣告MyButton這個class, 裡頭有一個叫做delegate的變數。

實作Delegate Methods
假如我們有一個叫做MyController的controller物件,要成為MyButton的
delegate, 我們會這麼做。首先是.h部分:

@interface MyController : NSObject  {
    IBOutlet MyButton *myButton;
}

@end
我們要先宣告MyController有實作MyButtonDelegate這個protocol,如此一來,
假如MyController漏了實作那些定義在MyButtonDelegate裡頭的method,
在編譯的時候就會跳出警告,要求我們修正,如果我們還是不實作的話,執行時,
就會發生找不到selector對應的實作的錯誤而crash。

如果MyController又是其他物件的delegate的話,我們可以在這段用大於與小於間包起來的宣告,繼續加入其他的protocol的名稱,例如<MyButtonDelegate, AnotherProtocol>。
至於一個物件是谷有實作某個protocol,我們可以用conformsToProtocol:檢查。

在MyController的實作中,我們就只要將myButton的delegate設成自已,然後實作該實作的method。我個人不太喜歡在protocol的宣告中出現大量註解,因為在實作protocol的時候,最方便的方式就是直接把protocol宣告的method複製貼上,接著逐一把肉放集protocol定義的骨幹裡;如果當中出現註解,複製貼上之後,還要把這些註解刪掉,其實還挺煩人。
@implementation MyController

- (void)awakeFromNib {
    [myButton setDelegate:self];
}

#pragma mark - MyButtonDelegate

- (void)myButtonDidBecomeClicked:(MyButton *)button
{
}
- (void)myButtonDidBecomeDoubleClicked:(MyButton *)button
{
}
- (void)myButtonDidBecomeTripleClicked:(MyButton *)button
{
}
- (void)myButtonDidBecomeQuadrupleClicker:(MyButton *)button
{
}

@end

我們在這邊透過setDelegate:指定delegate,如果我們把delegate宣告成是一個IBOutlet的話, 也可以直接在Interface Builder中連結。

Delegate Methods是怎麼被呼叫的?
MyButton是怎樣呼叫delegate的呢?其實很簡單。

@implementation MyButton

- (void)mouseDown:(NSEvent *)theEvent {
    switch ([theEvent clickCount]) {
        case 1:
            [delegate MyButtonDidBecomeClicked:self];
            break;
        case 2:
            [delegate myButtonDidBecomeDoubleClicked:self];
            break;
        case 3:
            [delegate myButtonDidBecomeTripleClicked:self];
            break;
        case 4:
            [delegate myButtonDidBecomeQuadrupleClicker:self];
            break;
        default:
            break;
    }
}

@synthesize delegate;
@end

注意事項
在上面的範例中,我們看到了設計delegate與protocol應該注意的地方:
Delegate物件不應該指定Class
我們將delegate物件宣告成id<MyButtonDelegate> delegate,意思就是
不需要管這個物件是屬於那個class,只要是個Objective-C物件即可,但是這個
物件必須實作MyButtonDelegate protocol。

我們其實可以將delegate物件是那個class寫死,例如把MyButton的delegate的class
指成MyController,但這樣做非常不好,如此一來,就只有MyController可以使用MyButton,其它controller都無法使用,就大大減少了重複使用MyButton的彈性。

Delegate這種設計方式,也方便我們在同時開發Mac OS X與iOS跟平台專案時共用程式碼,我們在撰寫某個model物件的時候,只使用Foundation或是其他兩個平台都有的framework,至於與平台相依的部分,就放進delegate中,然後在Mac OS X與iOS上各自實作delegate物件。

總之,在實作delegate的時候delegate屬於那個class並不重要,重要的是delegate物件有沒有實作我們想要呼叫的method。

Delegate屬性應該要用Weak,而非Strong
在使用property語法的時候,如果這個property是Objective-C物件,我們照說應該要設定成strong或retain,但是遇到的是delegate,我們應該設成weak或assign。

原因是:需要設計delegate物件這個物件,往往是其delegate物件的成員變數。
在我們的例子中,MyButton的instance是myButton,是MyController的成員變數,
自已可能已經被MyController retain了一份。如果MyButton又retain了一次MyController,就出循環retain的問題,我已經被別人retain,我又把別人retain一次。

如此會造成我們無法釋放MyController:在該釋放MyController的時候,MyController還是被自已的成員變數retain,MyController得要走到dealloc才會釋放myButton,但是自已卻因為被myButton給retain起來,而始終走不到dealloc。

Delegate Method的命名方式
Delegate method的命名有個鮮明的特色,就是這個method至少會傳入一個參數,就是把到底是誰呼叫了這個delegate method傳遞進來。同時,這個method也往往以傳入的class名稱開頭,讓我們可以辯識這是屬於那個class的delegate method。以UITableViewDelegate為例,假如我們在iOS的表格中選擇了某一列,就會呼叫

- (void)tableView:(UITablewView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

Method的名稱就以「tableView」開頭,讓我們知道這是屬於Table View的delegate method,然後第一個參數這個Table View的instance傳入,接下來才傳入到底是那一列被選起 資訊。

至少把是誰呼叫了這個deegate method傳入的理由很簡單。
以我們的MyController為例,這 個controller可能有好幾個MyButton,而這些MyButton
全都把delegate指到同一個controller上,那麼controller就需要知道,到底是被那個button呼叫。判斷方式只要簡單比對指標就好了:

-(void)myButtonDidBecomeQuadrupleClicked:(MyButton *)button
{
    if(button == myButton1) {

    } else if (button == myButton2) {

    }
}

[iOS] kkbox-ios-dev note Delegate與Protocal

在閱讀這一章之前,相信你應該已經寫過一些簡單的iOS程式,
知道如果我們想要一個像是系統設定(Setting.app)那樣的表格介面,
我們會建立UITableViewController的subclass,這個controller的view是
UITableView。

當我們想要設定表格中的內容,像是這個表格中有多少個section,
每個section裡有多少row、每個row裡面又是那些內容...我們不是直接呼叫
UITableView的method,像是呼叫[myTableView setSectionCount:3]或是
[myTableView setRowCount:3 atSection:4],而是去實作UITableViewController
裡面幾個像是template method的東西,像numberOfSectionsInTableView:、
tableView:numberOfRowsInSection:等等。

為什麼不是直接去改變view而是controller要準備一些不知道會被誰呼叫的method?
理由是:UITableViewController是UITableview的data source與delegate。
那什麼是delegate?

用我的話來說delegate就是將眾多的callback,集中在一個物件上。

從其他平台來看Objective-C的Delegate
Delegate算是入門Objective-C的另外一個障礙,但了解之後就會知道其實非常簡單,
一開始可能覺得並不好懂的主要原因是,delegate的這套作法,跟其他平台在做同樣的事情的時候,作法比較不一樣。

如果你曾經在其他的平台上開發過應用程式,像是微軟的.Net平台的話,可以發現,在C#語言中在講一個class有那些成員的時候,會包括properties、methods以及events;但是我們在講Objective-C的class時,並不會提到events,C#中使用events在做的事情,在Objective-C中,我們往往使用target/action與delegate實作。

雖然在Mac OX X與iOS分別有NSEvent與UIEvent,但是這邊的events與.Net裡面講的events又是兩件事情:NSevent是用來描述鍵盤按了那個按鍵,滑鼠移到了什麼位置,而UIEvent是用來描述觸控事件以及耳機上的播放控制按鈕等等,單純用來描述從硬體輸入了什麼事件,透過作業系統傳到我們的應用程式中。而C#裡面所稱的events,則是用來處理callback。

所謂callback是指,當我們呼叫了一個function或method之後,可能會花上許多時間,或是計算的是大量資料,或是需要透過網路連線,所以我們並不馬上要求得到回傳的結果,而是等到一段時間之後,計算結果才會透過另外一個function/method傳回來。

現在絕大多數的應用程式開發環境都採用MVC架構,我們將物件分成三類:model、view、controller,雖然在不同的平台上往往實作出來的結果不太一樣,像是在微軟的平台上,往往把window(或是frame)當作是controller由window物件責管理放在這個window上面所有的control元件,但是Mac OS X上面window並不拿來當做controller,而是被當成view,controller在window之外,而且一個controller也可以控制多個winodw...但都大抵如此。

在MVC的架構中,我們通常會先建好controller class,然後加入model與view,變成controller的成員變數;因為model與view是controller的成員變數,所以controller可以直接呼叫model與view,那麼,當model與view發生了變化,要回來通知controller,我們也可以稱之為callback,例如,controller建立了一個按鈕,但是直到按鈕被點選之後,controller才負責做事。

在C#中我們要處理一個event,就要提供一個event handler,我們現在要處理的是Click:
private void InitializeComponent() {
 this.button1 = new System.Windows.Forms.Button();
 this.button1.Click += Button1_Click;
}

private void Button1_Click(object sender, System.EventArgs e) {
}
如果只是單純的點選事件的話,我們會用target/action實作。但,對照.Net framework裡頭,events不只是click而已,還可能會有double click,triple click,quadruple click...,只果是C#裡頭,會這麼寫:

this.button1.Click += Button1_Click;
this.button1.DoubleClick += Button1_DoubleClick;

變成Objective-C的話就可能變成
 [button1 setTarget:self];
[button1 setAction:@selector(click:)];
[button1 setDoubleTarget:self];
[button1 setDoubleAction:@selector(doubleClick:)];
這樣寫起來實在很讓人煩躁(雖然NSTableView也的確有setDoubleAction:...) 所以當這樣的東西一多,在Objective-C語言裡頭,會直接準備好一個物件,這個物件準備好了 所有可以呼叫的method,這個物件就叫做delegate,而這些可以呼叫method的集合,叫做protocol。 好這個物件之後,我們就不用呼叫那麼多setDoubleAction,只要呼叫setDelegate。

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

本章主要討論跟UIViewController相關的記憶體相關問題。嚴格說起來比較像是在討論UIViewController的life cycle。

總之我們要回答的問題是,當我們建立了一個UIViewCOntroller之後,Xocde給我們的template中,會叫我們實作一個叫didReceiveMemoryWarning:的method,然後你可能從一些相關文件上知道,當系統記憶體不夠的時候,我們應該要在這個method裡頭釋放一些記憶體,那麼有那些記憶體是應該要釋放的?我們應該怎麼實作這個method?

記憶體不足警告(Memory Warnings)
在Desktop作業系統中,如果實體記憶體不足,應用程式使用的記憶體量,超過實體記憶體的數是,這時候作業系統會自動將記憶體中的部分資料,存入磁碟的虛擬記體(Virtaul Memory)當中,需要使用的時候,再從虛擬記體中載回實體記憶體。

iOS在發展之初到現在,都沒有虛擬記憶體,而是會在記憶體快要用完的時候,對應用程式發出記憶體不足的警告,要求釋放一些可以暫時不需要用到的物件,讓應用程式可以有足夠的記憶體繼續運作。如果無視記憶體警告,繼續放任記憶體用量成長,系統最後便會強制要求終止應用程式。

在記憶體不足的時候,除了會對UIApplication的delegate(就是所謂的AppDelegate)呼叫applicationDidReceiveMemoryWarning:之外,也會對系統中所有UIViewController呼叫didReceiveMemoryWarning:如果我們想要知道那些記憶體是可以在didReceiveMemoryWarning:釋放的,不妨先回顧一下在iOS6之前,iOS是怎麼做的。

從iOS問世到iOS5,只要發生記體不足,就會把所有不在最前景的View Controller的view釋放掉。因為這些View Controller的view並不在畫面上,用戶根本看不到,所以暫時先放掉也沒關系。

iOS6之前記憶體不足時系統主動釋放View的行為
所謂不在最前景的view controller就是:假如我們今天有一個tab bar controller,tab bar裡頭有四個項目,對應到四個view controller,但是其實只會顯示一個,那麼在iOS6之前,只要發生記憶體警告的時,其他三個view controller的view就會被釋放。

在navigation controller的navigation stack裡頭,也只有最上面的view controller的畫面需要顯示,其他view controller的view也可以被釋放。

所以,如果你曾經在iOS6之前的環境上開發過iOS App,可能會遇到一些奇怪的bug,你把一些狀態直接記錄在view裡面,像是改變了一些label裡頭的文字,但是繼續做了一些操作,然後回到這個view之後,發現view莫名其妙的回復到初始值,原本放在label的文字不見了,其實就是遇到了記憶體警告的結果。

UIviewController負責管理在應用程式中每個會用到的畫面,最主要的property是view而這個property是使用Lazy Loading pattern實到。Lazy Loading就是:我們要去使用某個物件的時候,我們才去建立那個物件,避免在物件初始的時候就建立了所有的property,而達到讓初始物件這個動作加速的效過。

當我們在透過alloc、init、或initWithNibName:boundle:建立View Controller的時候,並不會馬上建立view,而是當我們呼叫view這個屬性的時候才會建立。我們以下面的程式為例:

// 建立 MyViewController的instance,這時候還沒建立view
MyViewController *controller = [[MyViewController alloc] initWithNibName:NSStringFromClass([MyViewController class]) boundle:nil];
// 在被加入到navigation stack的時候會去呼叫[controller view]
// 這時候view才被建立起來
[navigationController pushViewController:controller animated:YES];
[controller release];
用Lazy Loading的方式實作一個getter的方式大致如下。在我們自已的程式中,想要有效使用記體,我們可以嘗試這麼寫。

- (UIView *)view {
    if(!_view) {
        _view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    }

    return view;
}

不過,UIViewController在還沒有view,而要去建立view的時候,會呼叫的其實是loadView這個method, 在view成功載入之後,則會呼叫viewDidLoad。
我們雖然不知道蘋果到底是怎麼實作UIViewController,但不外乎類似這樣:

- (UIView *)view {
    if(!_view) {
        [self loadView];
        if(_view) {
            [self viewDidLoad];
        }
    }

    return view;
}

所以,如果你有天不小心寫出像下面的程式碼,就會進入無窮迴圈:因為呼叫[self view] 的時候發現沒有view,就會呼叫loadview,但loadView又去呼叫[self view]。
- (void)loadView {
    [self view];
}

在iOS6之前,如果某個View Controller不在最上層,發出記憶體警告時,系統就會通知這些View Controller把View指向nil;而當我們再次需要使用這個View Controller的時候,就會因為呼叫到view而把view重新載入回來。

所以我們要注意,viewDidLoad並不是UIViewController的Initalizer,雖然我們在開始使用某個view controller的時候,一定會呼叫到一次viewDidLoad,我們也通常會在這個地方,做一些初始化這個view controller的事情,但viewDidLoad是有機會在View Controller的life cycle中被重複呼叫好幾遍,在建立了view之後,view也可以再次指向nil,所以view controller可能會被重複釋放與載入view,viewDidLoad也會被重複呼叫。

所以在iOS6之前,你曾經遇到某個View Controller回復到初始值這樣的問題,就是:原本有狀態的view因為記憶體警告被釋放了,而我們如果在viewDidLoad再次被呼叫的時候,沒有正確還原狀態,自然只有初始狀態的view。

iOS如何知道那個View Controller位在最上層?

那麼,view controller 自已怎麼知道自已位在最前景呢?其實很簡單:view controller被放到最上層時,會被呼叫到viewWillAppear:以及viewDidAppear:,離開最上層時,會呼叫viewWillDisappear:與viewDidDisAppear:。
只有呼叫過viewWillAppear:以及viewDidAppear:,而沒有呼叫過viewWillDisappear:與viewDidDisappear:的View Controller就是位在最前景的View Controller。
我們經常會override viewWillAppear:這些method,在做override的時候,應該要呼叫一次super的實作,因為super的viewWIllAppear:這些method其實作了一些必要的事情,在iOS6之前是用來確保那些view該被釋放,雖然蘋果推出iOS6時,或許是認為像是iPhone5這樣的裝置在可用資源上遠遠超越過去的硬體,因此不再刻意釋放view,但呼叫一下super的實作還是比較保險。

所以我們應該要在didReceiveMemoryWarning:做什麼?
從過去的經驗來看,不在最上層的view其實是可以釋放,在iOS6之後,當我們遇到記憶體不足的時候,我們也可以選擇性的決定要不要釋放view,像web view這種記憶體怪物在沒用到的時候實在應該要放掉。
以下是蘋果的範程式:

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
    if ([self.view window] == nil) {
        self.view = nil;
    }
}

2015年11月16日 星期一

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

這一章主要討論ARC。
前一章提到,由於ARC是透過靜態分析,在Compile TIme決定應該要在程式碼
的那些地方加入retain,release所以,要用ARC基本上非常簡單,就是先把
原本要手動管理記憶體的地方,把retain、release都拿掉,在dealloc的地方
也把[super dealloc]拿掉。

要了解那些地方是weak reference
ARC有時候會在一些地方沒做retain,結果卻又自動多做了一次release最後
導致Bad Access的錯諤。我們在講Selector的時候提到,我們可以將target/action
與必要的參數合起來變成另外一種物件,叫做NSInvocation,在ARC環境下
從NSInvocation拿出參數時,就必須要額外注意記憶體管理問題。
比方說,我們現在要把對UIApplication要求開啟指定URL這件事情,
變成一個Invocation。

NSURL *URL = [NSURL URLWithString:@"http://kkbox.com"];
NSMethodSignature *sig = [UIApplication instanceMethodSignatureForSelector:@selector(openURL:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:[UIApplication sharedApplication]];
[invocation setSelector:@selector(openURL:)];
[invocation setArgument:&URL atIndex:2];

但假如我們用以下這段code的方式,把invocation拿出參數的物件的時候, 就會遇到Bad Access錯誤
    NSURL *arg = nil;
    [invocation getArgument:&arg atIndex:2];
    NSLog(@"arg:%@", arg);

之所以會crash的原因是,我們在透過getArgument:atIndex:拿出參數的時候, getArgument:atInde:並不會幫我們把arg多retain一次,而到了用NSLog印出tag 之後,ARC認為我們已經不會用而arg了,所以就對arg多做了一次release,
於是retain 與release就變得不成對。
我們要解決這個問題的方法是要把arg設為Weak Reference或Unsafe Unretained
讓arg這個Objective-C物件的指標不被ARC管理,要求ARC不要幫這個物件做任何自動的retain 與release,在這邊要使用__weak或__unsafe_unretained關鍵字。程式會像這樣:
    __weak NSURL *arg = nil;
    [invocation getArgument:&arg atIndex:2];
    NSLog(@"arg:%@", arg);

ARC也不會排除循環Retain(Retain Cycle)的狀況,遇到了循環Retain,
還是會造成 Memory Leak,循環Retain就是,A物件本身retain了B物件,
但是B物件又retain了 A物件,結果我們要在釋放A的時候才有辦法釋放B,
但是B又得要在B被釋放的時候,才會釋放A, 最後導致A與B都沒有辦法被釋放。
這種狀況最可能出現在:
1.把delegate設為strong reference。
2.某個物件的某個property是一個block,但是在這個block裡頭把物件自已給 retain了一份。
3.使用timer的時候,到了dealloc的時候才停止timer。

假如我們現在有一個view controller我們希望這個view controller可以定時更新, 那麼我們可能會用 +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 這個method建立物件,指定定時執行某個selector。

我們要特別注意, 在建立這個timer的時候,我們指定給timer的target,也會被timer retain一份, 因此,我們想要在view controller在dealloc的時候,才停止timer就會有問題: 因為view controller已經被timer retain起來了,所以只要timer還在執行, view controller就不可能走到dealloc的地方。
所以應該在viewDidDisappear的時候就要停止timer。

Foundation Framework裡頭的每個物件,都有對應的C實作,
這一層C的實做叫作Core Foundation,當我們在使用Core Foundation
裡頭的C型態時,像是CFString、CFArray等,我們可以讓這些型態
變成可以接受ARC的管理。這種讓C型態也可以被當作Objective-C物件,
接受ARC的記憶體管理的方式,叫做Toll-Free Bridged

Toll-Free Bridged有三個語言關鍵字: __bridge、__bridge_retained、以及
__bridge_transfer。
* __bridge會把Core Foundation的C資料型態轉換成Objective-C物件,
但是不會多做retain與release。
* __bridge_retained會把CoreFoundation的C資料型態轉換成Objective-C
物件,並且會做一次retain,但是之後必須由我們自已手動物叫CFRelease,
釋放記憶體。
* __bridge_transfer會把Core Foundation物件轉換成Objective-C物件,
並且會讓ARC主動添加retain與release。不見得每個Core Foundation型態
都有辦法轉換成Objective-C物件。

其它
Objective-C語言有了ARC之後,除了禁止使用retain、release這些關鍵字之外
也禁止我們手動建立NSAutoreleasePool,同時禁止了一些我們在ARC之前的程式寫作方式。包括我們不可以把Objective-C物件放進C structure裡頭,Compiler會告訴我們
語法錯誤。

在有ARC之前,我們之所以會把Objective-C物件放進C Structure裡,大概會有幾個目的,
其一是,假如我們有某個Class有很多成員變數,那我們可能會想以下這種寫法將成員變數分成群組:

@interface MyClass : NSObject {
    struct {
        NSString *memberA;
        NSString *memberB;
    } groupA;

    struct {
        NSString *memberA;
        NSString *memberB;
    } groupB;
}

@end
這樣如果我們想要使用groupA裡頭的memberA,可以用self.groupA.memberA呼叫。

另外一種目的,則是有時候,我們可能會想要刻意隱藏某個Objective-C Class裡頭
有那些成員變數。像下面這段code裡頭,我們原本有一個Class叫做MyClass,裡頭
有privateMemberA與privateMemberB兩個成員變數,原本應該直接寫在MyClass的宣告裡頭,但是我們卻刻意把這兩個成員變數包進_Privates這個C Structure裡頭,而原本放在MyClass成員變數宣告的地方,只剩下了一個叫做privates的指標,光看到這個指標,讓人難以理解這個Class裡頭到底有什麼東西。
@interface MyClass : NSObject {
    void *privates;
}

@end

typedef struct {
    NSString *privateMemberA;
    NSString *privateMemberB;
} _Privates;

@implementation MyClass

- (void)dealloc {
    _Privates *privateMembers = (_Privates *)privates;
    [privateMembers->privateMemberA release];
    [privateMembers->privateMemberB release];
    free(privates);
    privates = NULL;
    [super dealloc];
}

- (instancetype)init {
    self = [super init];
    if (self) {
        privates = calloc(1, sizeof(_Privates));
        _Privates *privateMembers = (_Privates *)privates;
        privateMembers->privateMemberA = @"A";
        privateMembers->privateMemberB = @"B";
    }

    return self;
}
@end
這種寫法其實是種程式碼保護的技巧,主要在防範class-dump,或是從
class-dump衍生出的class-dump-z這些工具。class-dump可以從編譯好的Binary中
還原出每個class的header,當我們從class-dump抽出別人的App的header,
看出有那些Class,每個Class有那些成員變數,有那些method,也就可以看出整個
App的架構大致如何。這種寫法就是讓別人用class-dump倒出我們App的headers時,
不會太容易就可以了解我們一些重要的Class是如何運作,不過對於做軟體破解的人來說,
其實只要花上時間,所有軟體都有辦法破解就對了。
總之有了ARC之後,我們都無法繼續使用以上兩種的程式寫作方式。

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需要對應的成員變數。

2015年11月11日 星期三

[iOS] 關於Xcode Other Linker Flags

關於Xcode Other Linker Flags

[iOS] kkbox-ios-dev note  Category

Category:不用繼承物件,就直接增加新的method,或替換原本的method。

什麼時候應該要使用Category?
如果想要擴充某個class功能,增加新的成員變數或method,
我們又沒這個class的程式碼,正規作法就是繼承,建立新的subclass。
那,我們需要在不用繼承,就直接增加method這種作法的重要理由,
就是我們想要擴充的class很難繼承。

ex
1. Foundation物件
2. 用Factory Method Pattern實作的物件
3. Singleton物件
4. 在專案中出現次數已經多不勝數的物件。

Singleton物件:某個class只有,也只該有一個instance,每次都只對這個
instance操作。而不是建立新的instance。

Category的語法很簡單,一樣是用@interface關鍵字宣告header,
在@implementation與@end關鍵字當中的範圍是實作,
然後在原本的class名稱後面,用中括弧表示新增的category名稱。
@interface NSObject (testCategory)
- (void)printNl;
@end

@implementation NSObject (testCategory)

- (void)printNl {
    NSLog(@"%@", self);
}

@end
如此一來,每個物件都增加了printNl這個method可以這麼呼叫:
[myObject printNl]

在存檔的時候,檔名的慣例是原本的class名稱加上category的名稱, 中間用加號連接,以我們剛建立的範例為例。 就是NSObject+testCategory.h和NSObject+testCategory.m

Category還可以有什麼用途?
除了幫原有的class增加新的method,我們也會在幾種狀況下使用category。
1 . 將一個很大的Class切成幾個部分
2. 替換原本的實作(最好避免,因為不知道Objective-C runtime載入category的順序)

Extensions
Objective-C語言中有一項叫做extensions的設計,也可以用來拆分一個很大的class
語法與category非常相似,但是不太一樣,在語法上,extensions像是一個沒有名字的category,在class名稱之後直接加上空的括弧,而extensions定義的method, 需要放在原本的class實作中。
@interface MyClass : NSObject
@end

@interface MyClass()
- (void)doSomething;
@end

@implementation MyClass

- (void)doSomething {
    //doSomething
}

@end

extension的用途

1.拆分Header
如果我們就是打算實作一個很大的class,但是覺得header裡頭已經列出了太多method,我們可以將一部分method搬到extensions的定義裡頭。另外,extension除了可以放method之外,也可以放成員變數, 而一個class可以擁有不只一個extension,所以如果一個class真的有非常非常多的method與成員變數, 我們可以把這些method與成員變數、放在多個extension中。

2.管理Private Methods
這其實是更常見的用除,我們在寫一個class的時候,內部有一些method不需要,我們也不想要放在public header中 但是如果不將這些method放在header裡頭,又會出現一個困擾:在Xcode4.3之前,如果這些private method在程式碼中 不放在其它method前面,
其他的method在呼叫這些method的時候,compiler會不斷跳出警告,而這種無關緊要的警告一多,我們往往會忽視真正的警告。
想要避免這些警告,要不就是把private method都放在最前面,但這樣並不能完全解決問題,因為private method之間也會相互呼叫,花時間確認每個method之間的呼叫順序並不是很經濟的事;要不就是都用performSelector:呼叫,但這樣問題更大,就像前面提到,在method改名、呼叫refactoring工具的時候,這樣非常危險。
蘋果提供的建義是,我們在.m或.mm檔案開頭的地方宣告一個extensions,
將private method都放在這個地方,如此一來,其他method就可以找到private method的宣告。從Xcode4開始至少到Xcode7都是如此,在Xcode中所提供的file template中,如果你選擇建立一個UIViewController的subclass,就可以看到在.m檔案的最前面幫你預留了一塊extension的宣告。

Category是否可以增加新的成員變數或屬性?
因為Objective-C物件會被編譯成C的structure,我們雖然可以category中增加新的method,但是我們卻不能夠增加新的成員變數。
在Mac OS X 10.6與iOS4之後,蘋果提出一套叫做Associated Objects的辦法,讓我們可以在category中增加新的getter/setter,觀念差不多是:既然我們可以用一張表格記錄一個class有哪些method,那我們不就也可以另外建一張表格,記錄有哪些物件與這個class相關?
要使用Associated Objects,我們需要匯入objc/runtime.h然後呼叫objc_setAssociatedObject建立setter,用getAssociatedObject建立getter,呼叫時要傳入:我們要讓那個物件與那個物件之間建立關連,關連時使用的是那一個key(型別為C字串)。在以下範中,我們在MyCategory這個category裡,增加一個叫做myVar的property。

必須先#import <objc/runtime.h>

@interface MyClass(MyCategory)
@property (retain, nonatomic) NSString *myVar;
@end

@implementation MyClass
- (void)setMyVar:(NSString *)inMyVar {
    objc_setAssociatedObject(self, "myVar", inMyVar, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)myVar {
    return objc_getAssociatedObject(self, "myVar");
}

@end


在setMyVar:中呼叫objc_setAssociattedObject時,最後一個參數OBJC_ASSOCIATION_RETAIN_NONATOMIC,是用來決定要用那一種記憶管理策略,管理我們傳入的參數,在我們的例子中,我們傳入的是一個NSString,是一個Objective-C物件,所以我們必須要retain起來。這邊可以傳入的參數還可以是OBJC_ASSOCIATION_ASSIGN、OBJC_ASSOCIATION_COPY_NONATOMIC、OBJC_ASSOCIATION_RETAIN以及OBJC_ASSOCIATION_COPY,與property語法使用的記憶體管理方法一致。而當我們的MyClass物件在dealloc的時候,所有透過objc_setAssociatedObject而retain起來的物件,也都會被一併釋放。

2015年11月10日 星期二

[iOS] kkbox-ios-dev note  Selector

kkbox-ios-dev 電子書筆記

Selector簡短的定義:Selector就是用字串表示某個物件的某個method。
用更術語的說法會是:Selector就是Objective-C的virtual table中指向實際執行
function point的一個C字串。
那Selector有什麼用途呢:因為method可以用字串表示,因此,某個method就會變成可以用來傳遞的參數。

檢查某個funtion是否存在:respondsToSelector

在Objective-C中同樣也有try...catch語法、但並不鼓勵在Objective-C語言中使用。原因與Objective-C的記憶體管理機制有關,如果大量使用try..Catch,會導致Memory Leak

Objective-C本身並不算有Garbage Collection的語言,雖然在Mac OS X 10.5嘗試在Objective-C上使做GC,但是成果實在不甚理想,如果大量使用容易造成Memory Leak,因此在推出iOS之後也不敢將這套機制用在行動裝置上,而是在iOS5時放棄在runtime管理記憶體,而是推出ARC(Automatic Reference Counter)在compile time時決定什麼時候應該釋放記憶體

呼叫performSelector:需要注意的地方

1.我們在呼叫performSelector:的時候需要注意幾點:
對super呼叫performSelector:
前面雖然提到,對一個物件直接呼叫某個method,或是透過performSelector:呼叫,意義是一樣的,但如果是對super呼叫,卻有不一樣的結果。如果是:

[super doSomething];

代表的是super的doSomething實作。但如果是:
[super performSelector:@selector(doSomething)];

呼叫的是super的performSelector:,最後結果仍然等同於[self doSomething]。

2.如果是用Refactor工具修改名字的話Xcode並不會把用performSelector:呼叫要執行這個method的名字替換掉只會出現簡短的訊息。

一個selector只會指向一個實作,因此Objective-C不會有C++、Java、C#等語言當中的overloading。所謂overloading,就是可以容許有多個名稱相同,但是參數型別不同的function或method存在,在呼叫的時候,如果傳入了指定型別的參數,就會呼叫到屬於該參數型別的那一組function或mehtod。在Objective-C當中,同一個名稱的method,就只會只有一套實作,如果有多個名稱相同的method,就會以最後在runtime載入的那一組,代替之前的實作。

但Objective-C這樣做又有另外一項優點:我們的App不需要連結特定版本的runtime與libraries,就算libraries中export出來的function換了位置,只要selector不變,還是可以找到應該要執行的C function所以舊的App在新版本的作業系統上執行時,新版本作業系統,並不需要保留舊版的Libraries,而避免了C++等語言中所謂DLL Hell問題。