2013年1月30日 星期三

[Android] 如何在程式中分辨開發版跟正式版

好久沒有寫部落格了
理由是去年初之前都是專心寫iOS
但最近還需要同時寫Web跟Android
導致我同時弄的東西太多
光看文件就沒空了
絕對不是因為之前沈迷D3
也絕對不是因為之前狂看冰與火之歌
也絕對不是因為下班後活動太多..... (踢飛..)

這篇是第一次拿Android來寫,就拿我最近遇到的小問題來做分享吧
情境是有些功能我們在開發版跟正式版會有不同的行為
這時候會是開發版用一套code
正式版需要另一套code
那需要怎麼做才可以用程式判斷出執行環境的差異呢?

首先因為Android是java-based的
並沒有像Objective-C有define來作選擇性編譯
而在Android的架構中也沒有明顯的development/release兩個版本的設計
我google了一下,比較類似的討論有
這篇stackoverflow的討論
當中我比較傾向是用Signature來去達到這個目標
因為只有這個是runtime可以分辨出差異,而且是程式中可以取得的
再來是我們發佈到google play (或是其他market)
應該都會用的是release key做簽署
所以只要能夠在runtime跟release key的signature做比較
就可以判斷執行環境是development或是production

有了這個方向,
先查到Android中取得Signature的方法如下
PackageInfo pkgInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
for(Signature signature : pkgInfo.signatures)
{
     System.out.println("cert: " + sigature.toCharsString());
}
你只要把你的production的signature印出來,並且hardcode進你的code
之後你做個字串比對就可以判斷是不是production environment了。

但是你會發現印出來的字串很長,大概3-400個bye吧 (隨便估@@)
總覺得不是很精簡。
這時候假掰的個性又出現了
那就來個SHA-1 hash之後再base64吧~ \(^O^o)
其實這是Facebook Android SDK的key hash給的靈感 XD
方法是找到你用來簽署所用的keystore,對他下以下的指令 (以下為Mac環境為例)
keytool -exportcert -alias youralias -keystore yourkeystore.keystore | openssl sha1 -binary | openssl base64
這時候你應該會印出一個類似下面這種字串
+YyPzaeOKYkleq9Rwtk7+Rett/o=

把這個hash放在code中,
並且加上以下代碼
private static final String SIG_HASH_PRODUCTION = "+YyPzaeOKYkleq9Rwtk7+Rett/o=";

public boolean isProduction()
{
     try
     {
          PackageInfo pkgInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
          for(Signature signature : pkgInfo.signatures)
          {
               String sigHash =getSignatureHash(signature);
               if(SIG_HASH_PRODUCTION.equals(sigHash))
               {
                    return true;
               }
          }
     }catch(Exception e){}

     return false;
}


public String getSignatureHash(Signature signature)
{
     try {
          MessageDigest md = MessageDigest.getInstance("SHA-1");
          byte[] hash = md.digest(signature.toByteArray());
          String hashb64 = Base64.encodeToString(hash, Base64.NO_WRAP);
          return hashb64;
     } catch (NoSuchAlgorithmException e) {
          e.printStackTrace();
     }

     return null;
}

如此一來,只要用給google play上架簽署的apk,
執行時期的isProduction()都會回傳的是true
如此你可以在任何程式去用這個Utility function判斷是否為production mode。



2013/2/4 Updated
上面的方法有一個缺點就是,預設是development。
比較好的方法還是預設市production mode,
因為避免"意外"發生而變成development mode。
而developement的key是在~/.android/debug.keystore
command如下
keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore | openssl sha1 -binary | openssl base64
而程式中改成用debug has key做負面表列來判斷是否為production mode。
或是直接改成isDebugMode()

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

2012年1月20日 星期五

push notification - step by step

所謂萬事起頭難
Push notification更是如此
由於Push notification因為牽扯到iOS app, provider server跟APNS三方溝通
provider server又跟APNS中間溝通要透過SSL
再加上有些東西需要在provisioning portal做設定
所以我覺得對初學者來講第一步真的很困難的

最近在搞這個,我就把我實驗出可以work的方法記錄下來
除了當做自己的備忘以外
也同時提供也有同樣需求的做參考

主要有幾個步驟,測試請一定要在device上做,push notification無法在模擬器上模擬
1. 在xcode新增一個App,並且在AppDelegate加上以下的code
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.mainViewController = [[MainViewController alloc] initWithNibName:@"MainViewController" bundle:nil];
    self.window.rootViewController = self.mainViewController;
    [self.window makeKeyAndVisible];
   
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:UIRemoteNotificationTypeAlert];
    return YES;
}


- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    NSLog(@"receive deviceToken: %@", deviceToken);
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
    NSLog(@"Remote notification error:%@", [error localizedDescription]);
}


先來執行看看..
當然,不可能這樣就ok。你應該會在Debug訊息中得到這個error
Remote notification error:找不到應用程式的有效“aps-environment”授權字串
不用擔心,那是因為你還有些事情還沒做

2.
到iOS Provisioning Portal,
首先你要先產生一個app id
而這個app id的bundle id要跟你前面產生的app的bundle id一致。
(所謂的bundle id就是類似com.example.APNTest)

3.
產生完app id之後,點進這個app id
我們可以看到這個畫面..
請打開push notification的功能。
因為我們是測試,所以先以development為例。
按下"Development Push SSL Certificate"的Configure按鈕
此時會看到下面的畫面,按照裡面的動作產生Certificate Signing Request


他告訴我們要使用keychain access這個app
並且在選單上找到這個選項

點了之後,輸入你的email跟certification name

產生之後save to disk,並且回到ios provisioning portal
按continue之後會看到這個畫面

指定剛剛的CSR則可以得到這個畫面

之後就把這個Certification抓下來

抓下來記得點兩下安裝,這時候你的Keychain access應該會出現新的Cert.跟Private Key

4.
好了之後,回到iOS provisioning portal
切到Provisioning的Development,按下New Profile
產生一個新的Provision Profile是對應到你的bundle id的
產生完之後,抓下來點兩下,就可以安裝到你的xcode

5.
回到XCode
重新run你的app
應該就會成功的取得device token
receive deviceToken: <c87c312f 1efc...>
如果還是不行,請手動指定你的provision profile到你剛剛產生download下來的那個profile..

6.
上面是已經成功的把device跟app註冊到APNS了
接下來我們要做的是去讓Server上的程式去notify我們的app
因為要用SSL
我們要先產生p12檔才可以建立SSL
回到keychain access
選擇你剛剛的certification按右鍵
選擇export就可以輸出成p12了

7.
由於跟provider server跟APNS溝通的部分跟iOS無關
所以我寫了一個簡單的java程式去測試
細節就不講了
大家可以從這邊抓下來去使用
解開後,把剛剛輸出的.p12檔也放在該目錄之中
打開Console,到這個解開的目錄
並且打下下面的command
java -cp apns-0.1.5.jar:. APNTest "APNTest.p12" "APNTest" "c87c..." "Hello APN"
最後四個參數分別是
(1) 你的.p12的檔名
(2) 你的.p12的密碼
(3) 你的device token。從你上面印出來的token除掉中間空格就是了
(4) 你要送的message。

如果你看到下面的畫面
恭喜你,你踏出push notification的第一步了








2011年12月23日 星期五

Simple template engine for objective c

所謂template engine就是我們可以先定義一個template
例如一個html檔案
然後我們會希望把裡面少部分的片段透過程式去取代成我們想要的字串
而做這些取代動作的東西我們就稱作template engine

其實在iOS當中已經有一個非常簡單的template engine了,
也就是+[NSString stringWithFormat:(NSString *)format, …]
這東西就很像在c使用sprintf(...)
然而,這東西對大部份來時候來講都很好用,但是也有一些限制,
例如它必需要要一個蘿蔔一個坑 (傳進的參數就是對應到format中的每個%開頭的取代標示)
你不能要求一個蘿蔔兩個坑 (一個參數取代兩個取代標示)
甚至第一個蘿蔔一定要放在第一個坑 (第一個參數一定要放在第一個取代標示)

我查了一下stackoverflow
但是沒有找到我想要的答案
http://stackoverflow.com/questions/2539556/objectivec-builtin-template-system
得到的答案不外乎就是這幾個
1. 用上面提的+[NSString stringWithFormat:(NSString *)format, …]
2. 用-[NSString stringByReplacingOccurrencesOfString:withString:]
然後一個一個把取代標示換成我們要的字串
3. 不然就是給我一個非常強大的template engine..

其實這東西不難做,既然我們都已經有好用的NSScanner了,那我們就做一個簡單的template engine吧。
使用上我希望長這樣
NSString* testTemplate = @"Hello $[name], welcome to $[city].";

NSString* testString =
    [NSString stringWithTemplate:testTemplate
                         fromMap:[NSDictionary dictionaryWithObjectsAndKeys:
                                  @"Popcorny", @"name"
                                  @"Taipei", @"city",
                                  nil]];
NSLog(@"%@", testString);   // output: Hello Popcorny, welcome to Taipei."


程式碼如下