2015年12月14日 星期一

[iOS] kkbox-ios-dev Notification Center

Notification Center 

Notification Center是在Cocoa/Cocoa Touch Framework中,物件之間可以不必需要知道彼此的存在,也可以互相傳遞訊息,交換資料/狀態的機制。

我們可以把Notification Center想像是一種廣播系統。
當一個物件A的狀態發生改變,而有多個物件需要知道這個物件發生改變的狀況下,
物件A不必直接對這些物件發出呼叫,而是告訴一個廣播中心說:
「我的狀態改變了」,至於其他需要聽取狀態的物件呢,
也只要對這個廣播中心訂閱(subscribe)指定的通知,
所以當物件A發出通知的時候,這個廣播中心就會通知有訂閱通知的其他物件。
這個廣播中心就是Notification Center。 

我們經常使用Notification Center處理來自作業系統的事件。
假如我們現在寫了一個日記軟體,這個軟體裡頭已經有很多view,
每個view裡頭都有一篇日記,每篇日記上都有該篇日記的撰寫日期與時間。
我們通常會使用NSDateFormatter,使用系統偏好中的語系(Locale)設定,
將日期轉成符合語系設定的字串顯示,那麼,當用戶調整了系統偏好設定,
像是把中文改成英文,那麼我們原本用中文顯示的日期,
也應該馬上變成英文顯示-我們該怎麼做呢?

 我們最常使用的通知中心是NSNotificationCenter這個class,我們也通常使用這個class的singleton物件default center(也就是說,其實Notification Center有好幾個,
不過我們最常用的還是這個)。當系統語系改變的時候,Notification Center就會
發出叫做NSCurrentLocaleDidChangeNotification的這項通知。
所以,我們所有要顯示日期的畫面物件,都應該要訂閱這個通知,
在收到通知的時候,就要重新產生日期字串。

接送與發送Notification

 接收Notification

一個通知分成幾個部分
1. object:發送者,是誰送出了這個通知
2. name:這個通知叫什麼名字
3. user info:這個通知還帶了那些額外資訊 所以當我們想要監聽某個通知的時候,
便是指定要收聽由誰所出、那個名字的通知,並且指定負責處 理通知的selector,
以前面處理locale改變的例子來看,我們就會寫出這樣的code

- (void)viewDidLoad {
   [super viewDidLoad];
   // Do any additional setup after loading the view from its nib.
   [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localDidChange:) name:NSCurrentLocaleDidChangeNotification object:nil];
}

- (void)localeDidChange:(NSNotification *)notification {
 //處理locale改變的狀況
}

- (void)dealloc {
 [[NSNotificationCenter defaultCenter] removeObserver:self];
}

意思就是,我們要指定name為NSCurrentLocaleDidChangeNotification的通知,
交由localeDidChange:處理。在這邊object設定為nil,代表不管是那個物件送出的,
只要符合NSCurrentLocaleDidChangeNotification的通知,我們統統都要處理。

每個通知當中,還有可能會有額外的資訊,會夾帶在NSNotification物件的userInfo
屬性中,userInfo是一個NSDictionary。

像是我們如果讓某個text field變成了first responder,那麼,在iOS上,就會出現螢
幕鍵盤,而當螢幕鍵盤出現的時候,我們往往要調整畫面的layout,而螢幕鍵盤
在不同輸入法的大小不一樣,像是英文鍵盤比較起來,中文輸入鍵盤上面往往會有
一塊組字區,而造成中文鑑盤比英文鑑盤大。在螢幕鍵盤升起來的時候,我們會收到
UIKeyboardWillShowNotification這項通知,而這項通知就會用userInfo告訴我們
鍵盤尺寸與位置。

如果我們在-addObserver:selector:name:object:裡頭,把name指定會nil,
就代表我們想要訂閱所有的通知,通常不太會有這種情境,不過有時候你想要知道
系統內部發生了什麼事情,可以用這種方式試試看。

當我們不需要繼續訂閱某項通知的時候,記得對Notification Center呼叫
-removeObserver:,以上面的程式為例,我們在addObserver的時候傳入了self,
在removeObserver的時候,就要傳入self。我們通常在dealloc的時候停止訂閱。

在iOS4與Mac OS X10.6之後,我們可以使用
-addObserverForName:object:queue:usingBlock:這組使用block語法的
API訂閱通知,由於傳入block,所以我們就不必另外準備一個selector,可以將
處理notification的程式與add observer的這段呼叫寫在一起。而remove observer
的寫法也會不太一樣:-addObserverForName:object:queue:usingBlock:會回
傳一個observer物件,我們想要停止訂閱的時候是對-removeObserver:傳入
之前拿到的observer物件,範例如下。

Add observer的時候:

NSNotificationCenter *observer = [[NSNotificationCenter defaultCenter] addObserverForName:NSCurrentLocaleDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        //處理locale改變的狀況。
    }];
[[NSNotificationCenter defaultCenter] removeObserver:observer];

發送Notification
至於要發送notification,則是在建立了notification物件之後,對NSNotificationCenter
呼叫-postNotification:即可。
這三組method都可以用來發送notification。

- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo

Notification與Threading
當我們訂閱某個notification之後,我們並不能夠保證負責處理notification的selector
或block會在那個thread執行:這個notification是在那條thread送出的,
負責接受的selector或是block,就會在那條thread執行。

在慣例上,絕大多數的notification都會在main thread送出,之所以說「絕大多數」
,就是因為有例外,像是在iOS上,如果我們接上耳機、拔除耳機,或是將音樂透過
AirPlay送到Apple TV的時候,系統會透過AVAudioSessionRouteChangeNotification
告訴我們音訊輸出設備改變了,這個通知就會發生在背景,而不是main thread。

不過當我們在撰寫自已的程式,要發送notification的時候,為了考慮其他開發者會
預期在main thread收到notification,所以我們也就在main thread發送notification,
像是透過GCD,把postNotification的呼叫送到dispatth_get_main_queue()上。

Notification Queue

有的時候,我們的程式可能會在很短的時間送出大量的notification,而造成資源
的浪費或效能問題。

以KKBOX來說,我們在歌單中的歌曲物件發生改動的時候,會透過notification
更新UI:一首歌曲可能會出現在多個歌單中,而我們可能用了很多不同的UI物件
來呈現不同張歌單,因此,像一首歌曲的播放次數改變,如這首歌被多播了一次,
歌曲物件就透過notification center,告訴每個跟歌單UI相關的物件重新讀取歌單資料。

照理說,只要有一首歌曲改變,就該發出這種通知,但假如我們現在做的事情是
歌單同步-把另一台裝置上的歌單資料,同步到我們這台裝置上,那麼改動的就
不只是一首歌曲,而是一大批歌曲,如果有十首歌,就送出了十次通知;但是,
其實UI只需要改動一次就好了,沒有重複更新十次UI的必要。

這時候我們就該用NSNotificationQueue。我們可以把NSNotificationQueue想成
Notification的發送端與notification_center之間的一個buffer,這個buffer可以
讓我們暫緩送出notification,而在一段緩衝期之內,決定我們是否要合併通知。
以前面的例子來看,我們就可以先把原本預計的十次通知先放進NSNotificationQueue當中,然後讓NSNotificationQueue幫我們把十次通知合併成只有一次通知。

我們要先建立一個NSNotificationQueue物件:

notificationQueue = [[NSNotificationQueue alloc] initWithNotificationCenter:[NSNotificationCenter defaultCenter]];

再來我們發送通知的程式原本像這樣:
NSNotification *n = [NSNotification notificationWithName:@"KKsongInfoDidChangeNotification" object:self];
[[NSNotificationCenter defaultCenter] postNotification:n];

改寫成這樣:
NSNotification *n = [NSNotification notificationWithName:@"KKSongInfoDidChangeNotification" object:self];
[notificationQueue enqueueNotification:n postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender forModes:nil];

我們在這邊傳入了NSNotificationCoalescingOnName與NSNotificationCoalescingOnSender,代表的就是請notification queue合併
名稱相同,發送者也相同的通知。

Mac上其他的Notification Center
在iOS上面我們通常會用到NSNotificationCenter,特別是NSNotificationCenter的
defaultCenter:不過在Mac OSX上,我們還有其他的notification center可以使用。

NSDistributedNotificationCenter
果在iOS上的限制較為格,一直以來都想辦法禁止跨App之間的通訊
(IPC, Inter-Process Communication)。不過自從Mac OS X出現以來,Cocoa Framework就有Distributed Objects這套IPC機制,讓不同App之間可以傳遞Objective-C物件,後來更推出來XPC,可以在不同App之間傳遞block。

NSDistributedNotificationCenter就是在Distributed Objects技術上建立的notification center,也就是,如果你對NSDistributedNotificationCenter發送了通知,便可以讓其他的App收到來自你目前所在App送出的通知。

NSWorkSpace的Notification Center
NSWorkSpace這個物件在Mac上代表的是Mac的桌上環境,如果你想要要求Mac OS X開啟另外一個App,處理某個檔案或URL(在iOS上我們會要求UIApplication來openURL,
但是在Mac上則是交由NSWorkSpace處理),或是取得某個檔案在Finder裡頭的代表圖示...等,就會用到NSWorkSpace。

跟NSWorkSpace相關的通知,像是某個App是否被成功開啟,你的Mac電腦是否離開了休...等等,都不會透過NSNotificationCenter的defaultCenter,而是要透過[[NSWorkSpace sharedWorkspace] notificationCenter]這邊的notification center,我們要選擇正確的notification center做add observer,才能正確收到通知。

CFNotificationCenter

CFNotificationCenter是NSNotificationCenter在Core Foundation中的C實作,一般來說,如果我們有比較高階的API可以使用的話,我們會盡量避免使用比較低階的API,所以,只要有NSNotificationCenter可以使用的場合,我們應該不會用到CFNotificationCenter。

比較有可能用到CFNotificationCenter的場合,大概是iOS8之後,Hosting App與Extension之間的溝通。iOS8之後推出了Extension,可以允許開發者撰寫Today Widget、Share Widget以及模擬鍵盤等功能,Apple Watch的Watch App也屬於Extension;每個Extension都是額外可以讓作業系統載入的Bundle,Extension與我們原本的Ap p(便是Hosting App)之間,可以用Shared Data共用資料,但是當App發生改變要通知Extension,
卻沒有比較直接的辦法。在蘋果有新的API之前,我們就會倚賴透過CFNotificationCenterGetDarwinNotifyCenter()取得的darwin notification center發送通知。

而即使我們可以發送通知,CFNotificationCenter用起來也不是很方便,主要原因是CFNotificationCenter不像NSNotificationCenter,在傳遞通知的時候可以夾帶user info。再來,就是像前面說的CFNotificationCenter是C API,而我們會盡量希望使用比較高階的API。

2015年12月9日 星期三

[iOS] kkbox-ios-dev note Blocks

Block是Cocoa/Cocoa Touch Framework中的匿名函數(Anonymous Functions)的實作。所謂匿名函式,就是一段具有物件性質的程式碼,這一段程式碼可以當作函數執行,
另一方面,又可以當作物件傳遞;因為可以當作物件傳遞,所以可以讓某段程式碼,
變成是某個物件的某個property,或是當做method或是function的參數傳遞,
就是因為這種特性,造成最常使用block的時機就是拿block實作callback。
在有block之前,在Cocoa/Cocoa Touch Framework上要處理callback,
最常見的就是使用delegate(此外也可以使用比較具有C語言風格的方式,傳遞callback function的pointer, 或是使用target/action pattern)。在iOS4有了block之後,
可以看到蘋果自已便大幅改寫了UIKit等Framework的API,把本使用delegate處理
callback的地方,都大幅換成了block。

Block的語法
一直以來還是有不少人不滿block語法,甚至有人成立了一個叫做fuckingblocksyntax.com的網站。這個網站的網域名稱不怎麼優雅,不過裡頭倒是清楚整理了我們應該如何宣告block。

在C#裡頭,我們可能會把所有的callback都叫做event,但是在Cocoa與Cocoa Touch的世界裡頭,我們只會把來自硬體的輸入叫做event。
由於block的主用途,就在於處理callback,所以block經常就用在各種非同步工作的callback上,要執行各種非同步的工作,就往往要使用thred。

什麼時候該用BLock?什麼時候該用Delegate?
即使block可以大幅取代delegate處理callback,但是從蘋果自已的API設計中可以看到,並不是所有的delegate都 被block取代,在Cocoa與Cocoa touch framework
中,仍然大幅度使用delegate。那麼,我們就要問:當我們在設計API的時候,什麼狀況下應該使用block?什麼時候又該使用delegate?

通常的區分方式是:如果一個method或function的呼叫只有單一的callback,
那麼就使用block,如果可能會有多個不同的callback,那麼就使用delegate。

這麼做的好處是:當一個method或function呼叫會有多種callback的時候,很有
可能某些callback是沒有必要實作的。

如果使用delegate實作,那麼,在delegate需要實作的protocol中,我們可以用
@required與@optional關鍵字區分哪些是一定需要實作的delegate method。

但相對的,用block處理callback,就會很難區分某個block是否是必須要實作:
在xcode6.3之前,Objective-C並沒有nullabel、nonnull等關鍵字,讓我們知道
某個property、或某個method要傳入的block可不可以是nil,我們也往往搞不清楚
在這些地方傳入nil,會不會發生什麼危險的事情。

舉個例子。在iOS7之後蘋果鼓勵開發者使用NSURLSession處理網路連線,NSURLSession
就充分表現了「單一callback用block,多重callback用delegate」這一點。
假如我們現在想要把KKBOX的官網首頁抓下來,我們只要建立一個
NSURLSessionDataTask物件,一般來說,我們只需要處理「這個連線做完事情的下一步
該做什麼」,所以一般也只需要實作這個task的completion handler,就是傳入
網路連線結束之後要執行的block;一般連線結束,大概就是成功抓到資料或是連線
失敗兩種狀況,所以我們可以透過data與error這兩物件判斷是哪種狀況:失敗的話error就
不會是nil,我們就要處理error,反之就要處理data。

    NSURL *URL = [NSURL URLWithString:@"http://kkbox.com"];
    NSURLRequest *request = [NSURLRequest requestWithURL:URL];
    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            // handle error
            return;
        }

        // handle data
    }];

    [task resume];
但NSURLSession本常還是具有delegate。我們在發送連線的時候,除了處理連線結束
後要做什麼之外,有時候也可能會想處理在連線中途所發生的其他狀況,像是:HTTP
連線收到302轉址,遇到有問題的SSL憑證、server要求用戶輸入帳號密碼,這些狀況
我們要不要提示使用者?或,如果這是一個傳遞大檔、很花時間的連線,我們有沒有必要顯
示連線進度條?這些狀況還是會傳遞給NSURLSession的delegate,而如果我們要處理這些狀況、就要實作以下這些delegate methods。

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                     willPerformHTTPRedirection:(NSHTTPURLResponse *)response
                                     newRequest:(NSURLRequest *)request
                              completionHandler:(void (^)(NSURLRequest * __nullable))completionHandler;
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                            didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 
                              completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler;
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                              needNewBodyStream:(void (^)(NSInputStream * __nullable bodyStream))completionHandler;

_block關鍵字
在一個block裡頭如果使用了在block之外的變數,會將這份變數先複製一份再使用,
也就是說,在沒有特別宣告的狀況下,對我們目前所在的block來說,所有外部物件的
變數都是唯讀,只能讀取不能變更。至於block裡頭用到的objective-C物件,則都會
被多retain一次。

如果我們想要讓某個block可以改動某個外部的變數,我們就要在這個需要可以被block
改動的變數前面,加上_block關鍵字。
這樣是不合法的程式:

    int i = 1;
    void (^block)(void) = ^{
        i = i + 1;
    };

應該寫成:
    __block int i = 1;
    void (^block)(void) = ^{
        i = i + 1;
    };

__weak關鍵字
在使用了block之後,記憶體管理會變得非常複雜,所以最好是在開啟了ARC自動記憶體
管理之後再使用block。不過,即使開啟了ARC,還是可能會遇到循環retain的問題。
由於block中用到的Objective-C物件都會被多retain一次,這邊所指的Objective-C物件
也包含self,所以,即使有個物件的property是一個block,而這個block裡頭又用到了
self,就會遇到循環retain而無法釋放記憶體的問題:self要被釋放才會去釋放這個property
,但是這個property作為block又retain了self導致self無法被釋放。

下面這段code就有循環retain的問題:
@interface MyClass : NSObject
- (void)doSomething;
@property (nonatomic, copy) void (^myBlock)(void);
@end

@implementation MyClass

- (instancetype)init {
 self = [super init];
 if (self) {
  self.myBlock = ^ {
   [self doSomething];
  };
 }
}

- (void)doSomething {
}
@end
如果我們不想讓self被myBlock給retain起來,我們就要把self變成weak reference
再傳入到block中。像是改成這樣:
__weak MyClass *weakSelf = self;
self.myBlock = ^ {
 [weakSelf doSomething];
}

Block作為Objective-C物件
前面提到Block其實可以當成是一種物件,我們接下來就要來看block作為物件的特性。

記憶體管理
由於Objective-C物件需要做記憶體管理,因此,如果你還在手動管理記體,在建立
block的時候,也必須手動管理Block的記憶體。我們可以使用Block_Copy()和
Block_Release()這兩個C Function對block做copy或release或,我們也可以把block
給cast成id型別,便可以對block呼叫copy與release。不過在啟用ARC之,我們便不需要
,Compiler也會阻止我們手動管理記憶體。

Block的型別
Objective-C當中每個物件都具有class,而每個class都繼承自NSObject,由於
block具有物件的性質,因此block本身也有class。不過,一個block是屬於那一種
class平時對我們來說並不會有太大的意義,畢竟我們在建立block的時候,並不會指定要
建立那一種class的block,我們也不會去subclass某種block的class。基本上,當我們
寫好一個block之後,這個block最後會變成那個class,全部都是由compiler決定。

在C語言當中,記憶體分成三塊:global、stack與heap,compier在編譯程式碼
的時候,會根據我們所寫出來的block到底使用到那一塊記憶體,將這個block變成不同的class,包括__NSGlobalBlock__、__NSStackBlock__與__NSMallocBlock__。知道這件事情通常對我們不會有什麼幫助、但是可以讓我們了解蘋果的compiler曾經發生過的bug:在某個狀況下,有個block應該要使用global的記憶體,但是compiler卻誤判成只使用stack的記憶體。

如果沒有開啟ARC,以下這段程式碼會在執行到block()這一行的時候,發生Bad Access錯誤:

- (NSArray *)blocks {
 int i = 1;
 return @[^{return i;}];
}

- (void)callBlock {
 int (^block)(void) = [self blocks][0];
 block();
}

原因是:在-blocks所回傳的NSArray中所包含的block物件中,使用到了i這個只出現在blocks這個 method內部的int變數,因為這個變數只在這個method中使用,
compiler便認為i應該使用stack的記憶體, 因此也把回傳的block建立成__NSMallocBlock__;於是當我們在-callBlock裡頭呼叫block()的時候,
原本的記憶體已經被釋放,於是產生記憶體管理錯誤。

那些事情不要拿Block來做

在很多狀況下,使用block相當方便,但由於因為block的記憶體管理問題,
有些事情使用block反而相當痛苦,就我個人而言,最痛苦的經驗應該就是拿block寫
遞迴。舉個例子,假如要使用block來寫一個費式數列,可能會寫這樣。

int (^fibs)(int) = ^(int n) {
 if (n == 0) {
  return 0;
 }
 if (n == 1) {
  return 1; 
 }

 return fibs(n-1) + fibs(n-2);
}

看起來好像沒有問題,但是一執行就會馬上crash










原因是在fibs這個Block的scope裡頭,fibs這個變數被指向NULL。一般來說,
對nil物件做任何Objective-C呼叫都沒事,但是如果一個Block變數指向NULL,
一呼叫就會因為Bad Access錯誤而crash。而當你寫出這段一定會crash的程式
compiler也都不會發出警告...誰說用了ARC就不會有記憶體管理問題呢?

那麼,我們在fibs前面加上個__block看看?

__block int (^fibs)(int) = ^(int n) {
 if (n == 0) {
  return 0,
 }
 if (n == 1) {
  return 1,
 }
 return fibs(n-1) + fibs(n-2)
}
結果這樣的程式會因為循環retain而造成記憶體漏水














這段Code要寫成這樣才不會有問題:
__block int (^fibs)(int) = ^(int n) {
 if (n == 0) {
  return 0,
 }
 if (n == 1) {
  return 1,
 }
 return fibs(n-1) + fibs(n-2)
}
fibs = fibs_;















Callback Hell

Block通常用在處理callback,而我們往往會在callback裡頭又做另外一件事情,
而這件事情又可能有另外一個callback,而這個callback裡頭要做的事情又
有另外一個callback....於是,我們可能會寫出這種深度非常深的程式碼:

[some object doSomethingWithCallback:^{
 [some object doSomethingWithCallback:^{
  [some object doSomethingWithCallback:^{
   [some object doSomethingWithCallback:^{
     // do something
   }];
  }];
 }];
}];
這種狀況我們稱之為Callback Hell--無限延長的Callback地獄,這種現象
除了會出現在Objective-C的block之外,也出現在各式各樣的程式語言中,
尤其是在JavaScript開發中,特別常討論Callback Hell。為了要解決Callback Hell,
從ECMAScript 6開始,就改成使用promise的寫法處理Callback,而像是
Parse的Bolts Framework,便是將Promis從JavaScript port到Objective-C中。

使用了Botls Framework之後,我們可以將各種要非同步執行的工作,包裝成
BFTask物件,我們便可以將上面那段code,改寫成這個樣子:
[[[[someObject doSomething] continueWithBlock:^id(BFTask *task)]{
 return [someObject doSomething];
}] continueWithBlock:^id(BFTask *task) {
 return [someObject doSomething];
}] continueWithBlock:^id(BFTask *task) {
 return nil;
}];

雖然一樣是callback之間不斷串連,但是在程式碼中,我們把callback之間從不斷加涫的關系, 變成了不斷往下的關系,也因此變得好讀。

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之後,我們都無法繼續使用以上兩種的程式寫作方式。