2012年2月28日 星期二

iOS blocks - 三個會造成retain cycle的anti patterns

在iOS4.0推出了Blocks這個語言特性後
到現在iOS都已經出到5.0了
所以我想Blocks應該可以被廣泛應用了
但現在iOS環境是從MRC(Manual Reference Counting) 走到ARC (Automatic Reference Counting)
在Reference Counting的環境中Runtime是無法自動解除Retain cycle的
而Blocks有很多隱性的retain的動作
很容易不小心的造成retain cycle。
而本篇的重點是點出三種會造成Retain Cycle的Anti-patterns
再來講一下怎麼解決。

在討論之前還是先大概重述一些概念
block當中是允許去使用外部的variable
但是local variable是會自動做retain的動作
例如
MyClass* foo = ….;
self.someBlock = ^{
    [foo bar];
};
上面的foo在此block被copy到heap的時候
也會一起被自動retain
而這就是我說的很容易造成retain cycle的主因。


Anti Pattern 1
第一個例子我們先用大家很常用的Opne source library ASIHttpRequest當作一個範例
看看下面的例子,有發現任何問題嗎?
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
    // Use when fetching text data
    NSString *responseString = [request responseString];

    // Use when fetching binary data
    NSData *responseData = [request responseData];
}];
這邊我們要首先要注意的點就是[request setCompletionBlock:…]這裡
很明顯的這邊的用途是要做一個event callback的用途
也就是說我給你一個block,當動作完成的時候callback我。
這是一個典型的非同步的作法。
但由於如果你要把block拿來之後使用,
你一定要呼叫[aBlock copy]的動作,
此動作會把block從stack丟進heap。
因為在iOS的環境block也是一個object,
此時這個block的retain count就會增加1
這時候根據定義,這個block中參照的request這個local變數就也會被retain起來。
所以request的retain count也會增加1。
但問題來了,一旦request完成任務,應當要被release的時候
卻會發現retain count始終無法歸零。理由是
request <-> block 這兩個互相retain
而無法正常釋放,這就是所謂的retain cycle了。

解決方法很簡單,看看ASIHttpRequest官網的文件
也就是用__block來描述request
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
    // Use when fetching text data
    NSString *responseString = [request responseString];
 
    // Use when fetching binary data
    NSData *responseData = [request responseData];
}];
通過block variable不會retain的特性,
有點類似weak reference的作用
此時block就不會retain request
當然也就不會有retain cycle的問題。


Anti Pattern 2
Anti Pattern 1是在使用別人的library的時候容易出現的
Anti Pattern 2是在實作自己的class的時候容易出現
請看下面這段code
//MyClass.h
@property  MyBlock onCompleteBlock;

//MyClass.c
self.onCompleteBlock = ^{
    [self doSomething];
}
我相信這邊大家已經馬上看出問題在哪裡了
其實Anti Pattern2算是Anti Pattern 1的特例
只是這邊使用的是特殊變數self

但有些時候我們更容易忽略的是在block中始用自己的member variable
例如
//MyClass.h
@interface MyClass : NSObject
{
    NSDate* lastModifed;
}

//MyClass.c
self.onCompleteBlock = ^{
    lastModifed = [[NSDate date] retain];
}
這時候就沒有那麼容易察覺了。
根據定義,在使用block的時候,
如果我們使用到member variable,
此時retain的不是lastModified指到的object
而是retain self
所以造成的就是
self <-> block 互相retain
跟anti pattern 1一樣的結果就是無法最終釋放記憶體。

這時候的解決方法也是一樣是拿出__block來用
//MyClass.c
__block MyClass* tempSelf = self;
self.onCompleteBlock = ^{
    tempSelf.lastModifed = [NSDate date];
}

Anti Pattern 3
繼續看下面的code
SettingsViewController* settingsViewController = 
   [[[SettingsViewController alloc] init] autorelease];
settingsViewController.onUpdate = ^{
    [self doUpdate];
}
self.settingsViewController = settingsViewController;
雖然這個Block中沒有直接使用到settingsViewController,感覺應該不會有retain cycle
但是因為self -> settingsViewController
而setttingsViewController -> block
再來block -> self
這就剛好繞了一圈,同樣會有retain cycle。

所以呢,還是要想一樣用anti pattern 2的解法去解決
//RootViewController.m
SettingsViewController* settingsViewController = 
   [[[SettingsViewController alloc] init] autorelease];
__block RootViewController* tempSelf = self;
settingsViewController.onUpdate = ^{
    [tempSelf doUpdate];
}
self.settingsViewController = settingsViewController;


在reference counting的環境裡,
我建議要解決retain cycle的最好思維就是想清楚從屬關係
例如最後一個anti pattern
他們的從屬關係應該就是
RootViewController -> SettingsViewController -> block
如果block要用到SettingsViewController或是RootViewController,
則就要使用weak reference (也就是__block)
在這樣的原則之下,就可以知道哪些要給他retain哪些不要了。

最後要補充一點就是上面的例子都是在MRC環境下當做範例
在MRC中__block variable在block中使用是不會retain的
但是ARC中__block則是會Retain的。
取而代之的是用__weak或是__unsafe_unretained來更精確的描述weak reference的目的
其中前者只能在iOS5之後可以使用,但是比較好 (該物件release之後,此pointer會自動設成nil)
而後者是ARC的環境下為了相容4.x的解決方案。
所以上面的範例中
__block MyClass* temp = …;    // MRC環境下使用
__weak MyClass* temp = …;    // ARC但只支援iOS5.0以上的版本
__unsafe_retained MyClass* temp = …;  //ARC且可以相容4.x以後的版本

相關文章
1. 探討Objective-C Block (part 1)
2. 探討Objective-C Block (part 2)
3. 探討Objective-C Block (part 3)

2012年2月6日 星期一

所見即所得再進化 - iOS5 Storyboard

StoryBoard絕對是iOS5讓人眼睛一亮的功能
在iOS5之前,我們已經可以透過Interface Builder中去編輯xib來設計View Controller的look & feel
而StoryBoard的導入更可以直接的把整個WorkFlow都也一併的加入
讓我們透過一個StoryBoard檔,就可以看得出整個應用程式的概括
對於開發的效率絕對是有所助益。

然而在我開始接觸StoryBoard之前,
我有幾個觀察重點
1. 能不能夠相容於iOS5以下的版本
2. 能不能完全取代.xib
3. 用了Storyboard之後會不會有什麼樣的限制?
4. 用了Storyboard之後,可否跟舊的.xib共存?
先來跟大家解答,再逐一解釋,
答案是1.No 2.Yes 3.No 4.Yes
除了1的答案比較可惜以外,其他應該是很正面的答案

1.的部分因為StoryBoard中多了幾個UIStoryboard的class,
而UIViewController也增加新的UIStroyboard的method,property
所以只能在iOS5之後使用,
這點我認為是唯一現在不開始用Storyboard的考量,其他都不是太大的問題。

2. 能完全取代.xib!! 事實上.storyboard可以說是.xib的集合,
並且又增加了view controller之間的關聯 (segue)
所以我認為我們可以直接透過.storyboard來做所有xib的事情,
並且集中在一個storyboard絕對是個好方法。

3. App中始用了Storyboard之後不會有什麼增加的限制。
雖然看起來Storyboard好像會把ViewController到另一個ViewController中的行為定的死死的,
但事實上不然。

首先,對於非常死的動作當然用Storyboard可以定義到說按下某個button之後,
navigate到哪個ViewController這種行為可以透過拖拉就完成,
不需要去寫code。

再來,如果是說要做到conditional action的動作。
例如可能想要做到說按下按鈕,在某種condition才會去做這個segue的動作。
這也是OK的,
因為我們還是可以透過UIViewController的-[UIViewController performSegueWithIdentifier:sender:]去用程式的方法去執行你的segue。
也就是說在這個需求的情況下,
我們應該是要自己去實作這個event的action
並且在你的情況成立時,再去透過上述的方法去執行你的segue。

再來就是segue也沒有一定要綁在某個UIControl上面,
我們可以直接定義一個UIViewController到另外一個UIViewController的segue
方法是直接在UIViewController上面control+click拖曳到另外一個UIViewController,
就可以新增一個segue
之後也是一樣是透過-[UIViewController performSegueWithIdentifier:sender:]去執行你的segue..

最後,你如果不想用segue的方法去瀏覽,
你還是可以用最傳統的方式,或是用xib的方式,產生你的UIViewController
再透過push或是modal的方式瀏覽你的view controller。

以上可知,用了storyboard只是把一些常用的動作簡化。但是複雜的行為還是可以用舊的方法達到目的

4. Storyboard可以跟xib並存是肯定的。甚至你的application bundle可以有多個.storyboard跟.xib並存都沒問題
我們可以透過+[UIStoryboard storyboardWithName:bundle:]去取得Storyboard的instance
再透過這個instance來產生initial view controller或是某個identifier的view controller,
可以視你的程式所需要的模組化需求來決定哪些要放在同一個Storyboard當中。
例如我可以把所有設定相關的部分放在Settings.storyboard當中,
而其他應用程式Workflow放在MainStoryboard.storyboard。
至於很獨立的UIViewController,則單獨定義一個.xib檔
並且透過-[UIViewController initWithNibName:bundle:]來產生。

另外說一下我覺得用Storyboard來取代xib的優點。
由於我們現在可以把UITabViewController, UINavigationController,
還有我們的所有自訂的ViewController連在一起。
以往我們要拖拉元件時,要手動告知Interface Builder說我的ViewController有navigation bar,有tab bar,
現在因為多了這些關聯變成一切更自動了。

以下面圖中的例子
原本是沒有UINaviationController的
我們就會看到沒有Navigation bar

如果我加上了UINavigationController
就會多出了NavigationBar

如果我再加上UITabViewController
則TabBar也會自動跑出來,
這也算是用了Storyboard才會有的好處。

我的結論是
如果你開發的app沒有要支援iOS5以下的target
那我會毫無考慮的建議你就直接用Storyboard吧!!!











2012年2月3日 星期五

Appcelerator Titanium一日遊

因為前一個project告一段落
而在下個project進來之前剛好有一些空擋
所以最近比較可以研究一下其他東西
今天就來玩玩前一陣子聽別人在用的PhoneGap或是Titanium這種cross-platform mobile application solution。
在網路上搜尋了一下
看到了這篇討論
Comparison between Corona, Phonegap, Titanium
細節我不說了
結論大概是PhoneGap支援比較多的平台
而Titanium有比較好的native UI整合
由於我希望讓使用者看不太出來是非native sdk寫出來的
所以我就選擇來玩玩Titanium!!
下面是我的"一日"心得報告,可能有所偏頗,所以大家參考參考就好,
如有錯誤也歡迎指教。

首先到這邊來下載Titanium SDK
目前我抓到最新版本是1.8.1
主要支援的是iOS跟Android目前最主流的這兩個mobile平台
Titanium主要是用Javascript去撰寫所有的app
No Java, No Objective-C, just only javascript!!!!
這對於從Web programming過來的programmer應該是超級福音
但是需要注意的是.. 你還是需要安裝Android SDK跟iOS sdk
也就是說如果你要跑iOS
你還是需要一台mac XD
不過那本來對我就不是個問題
我想要的只是希望Write once, and iOS and android version runnable..

再來就是開發環境
如果你以前已經有用過eclipse那你應該可以很熟悉這個環境
因為Titanium所用的開發環境TitaniumStudio就是用eclipse改過來的
所以開發環境非常的成熟而且整合的不錯

至於開發到底容易不容呢?
我只花了一天的時間就寫出了一個簡單的GPS定位自己的位置,
並且把google map秀出自己附近的地圖 真的不是很難寫..
但是強烈建議,把官方影片從101到104先看過一遍
再來把幾個sample project抓下來跑跑看,看一下他的程式架構
絕對大大的加快你熟悉開發環境的速度

經過一天的評估
我覺得如果將來有需要跨iOS跟Andriod的平台需求
我會很想要用Titanium去實作看看
畢竟要同時開發兩個平台的成本可能是1+1 > 2
通常也至少要兩個人才有辦法
而且同時會iOS跟Andriod的人才太少了
而Titanium只要一個code base就可以搞定兩個平台了
這點就太吸引人了

不過會讓我比較擔心的是網路上有人提到此平台還為數不少的bug
然後再來就是畢竟多包了一層
所以平台的掌握度也跟直接用原生SDK不能比
至於值不值得
等到我真的有用Titanium完成一個專案後再來分享吧 :D










2012年2月1日 星期三

Push Notification簡介

上一篇文章的push notification - step by step主要是以實作來去撰寫
但想說網路上中文的push notification的文章真的很少
那我就再來寫一篇來介紹ios push notification

push notification主要由三方溝通
也就是iOS device, provider, APNS (apple push notification service)
其中iOS device中又可以把iOS系統層跟application分開來


1. iOS跟APNS的SSL連線
這個連線是我們開發者不會直接接觸的,
是iOS系統層跟APNS索建立的連線
透過這個唯一的連線
iOS系統可以統一的處理所有application跟apns中間的溝通
裡面走的是jabber/xmpp的protocol, APNS port是5223

2. provider跟APNS的SSL連線
這個連線是讓provider server去通知APNS去notify device用的
這個SSL certification key是要在iOS provision portal在app id那邊產生的
裡面走的protocol是apple定義的一個binary protocol
又分simple跟advanced兩種格式
詳見 http://goo.gl/2EB9J
而APNS port是2195

3. Application在啓動的時候需要呼叫- [UIApplication registerForRemoteNotificationTypes:]
來去跟iOS註冊要使用push notification
如果註冊成功的話,iOS會回呼 -[UIApplicationDelegate application:didRegisterForRemoteNotificationsWithDeviceToken:]
此時可以取得一個32bytes的device token

4. 當第三步的registration成功後,application需要跟provider告知我的device token是什麼。
因為這個是屬於application developer的自訂邏輯,
怎麼樣跟provider server告知這個device token完全看自己的需求設計
而provider要做的就是要把iOS傳來的device token放進provider的DB當中,
以便當有通知要觸發的時候,可透過provider<->APNS的connection來觸發通知的動作


至於當Device收到push notification之後會發生什麼事情?
如果App是在還沒被啟動的狀況下被呼叫,則device會出現類似下面的畫面。
使用者如果選擇執行這個notification,
則你的應用程式會被打開
並且呼叫 - [UIApplicationDelegate application:didFinishLaunchingWithOptions:]
透過第二個參數的地方可取得此notification的資料

相對的,如果我們的App在notification來之前已經是在執行的狀態了
則底層會改呼叫- [UIApplicationDelegate application:didReceiveRemoteNotification:]


push notification的packet可以做的事情不多
裡面只能放alert body, badge number, sound, launch image, 以及custom defined key/value
而這些資料要以json格式描述並且要在255 bytes之中…
但最重要的是,它提供了一個完美機制讓我們的應用程式即使沒有在運作,
但透過遠端的觸發,讓事件發生時,可以通知使用者喚醒你的App。


Reference
[1] Local and Push Notification Programming Guide
[2] Using Local and Push Notifications