tag:blogger.com,1999:blog-82608444455274942642024-03-06T01:03:08.259+08:00popcorny的碎碎唸popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.comBlogger25125tag:blogger.com,1999:blog-8260844445527494264.post-58761567908553628122013-01-30T22:40:00.000+08:002013-02-04T11:57:31.253+08:00[Android] 如何在程式中分辨開發版跟正式版好久沒有寫部落格了<br />
理由是去年初之前都是專心寫iOS<br />
但最近還需要同時寫Web跟Android<br />
導致我同時弄的東西太多<br />
光看文件就沒空了<br />
絕對不是因為之前沈迷D3<br />
也絕對不是因為之前狂看冰與火之歌<br />
也絕對不是因為下班後活動太多..... (踢飛..)<br />
<br />
這篇是第一次拿Android來寫,就拿我最近遇到的小問題來做分享吧<br />
情境是有些功能我們在開發版跟正式版會有不同的行為<br />
這時候會是開發版用一套code<br />
正式版需要另一套code<br />
那需要怎麼做才可以用程式判斷出執行環境的差異呢?<br />
<br />
首先因為Android是java-based的<br />
並沒有像Objective-C有define來作選擇性編譯<br />
而在Android的架構中也沒有明顯的development/release兩個版本的設計<br />
我google了一下,比較類似的討論有<br />
<a href="http://stackoverflow.com/questions/1743683/distinguishing-development-mode-and-release-mode-environment-settings-on-android">這篇stackoverflow的討論</a><br />
當中我比較傾向是用Signature來去達到這個目標<br />
因為只有這個是runtime可以分辨出差異,而且是程式中可以取得的<br />
再來是我們發佈到google play (或是其他market)<br />
應該都會用的是<a href="http://developer.android.com/tools/publishing/app-signing.html#releasemode">release key做簽署</a><br />
所以只要能夠在runtime跟release key的signature做比較<br />
就可以判斷執行環境是development或是production<br />
<br />
有了這個方向,<br />
先查到Android中取得Signature的方法如下<br />
<pre class="code">PackageInfo pkgInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
for(Signature signature : pkgInfo.signatures)
{
System.out.println("cert: " + sigature.toCharsString());
}
</pre>你只要把你的production的signature印出來,並且hardcode進你的code<br />
之後你做個字串比對就可以判斷是不是production environment了。<br />
<br />
但是你會發現印出來的字串很長,大概3-400個bye吧 (隨便估@@)<br />
總覺得不是很精簡。<br />
這時候假掰的個性又出現了<br />
那就來個SHA-1 hash之後再base64吧~ \(^O^o)<br />
其實這是<a href="https://developers.facebook.com/docs/getting-started/facebook-sdk-for-android/3.0/">Facebook Android SDK</a>的key hash給的靈感 XD<br />
方法是找到你用來簽署所用的keystore,對他下以下的指令 (以下為Mac環境為例)<br />
<pre class="code">keytool -exportcert -alias youralias -keystore yourkeystore.keystore | openssl sha1 -binary | openssl base64
</pre>這時候你應該會印出一個類似下面這種字串<br />
<code>+YyPzaeOKYkleq9Rwtk7+Rett/o=</code><br />
<br />
把這個hash放在code中,<br />
並且加上以下代碼<br />
<pre class="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;
}
</pre><br />
如此一來,只要用給google play上架簽署的apk,<br />
執行時期的<code>isProduction()</code>都會回傳的是<code>true</code><br />
如此你可以在任何程式去用這個Utility function判斷是否為production mode。<br />
<br />
<br />
<hr>2013/2/4 Updated <br />
上面的方法有一個缺點就是,預設是development。<br />
比較好的方法還是預設市production mode,<br />
因為避免"意外"發生而變成development mode。<br />
而developement的key是在~/.android/debug.keystore<br />
command如下<br />
<pre class="code">keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore | openssl sha1 -binary | openssl base64</pre>而程式中改成用debug has key做負面表列來判斷是否為production mode。<br />
或是直接改成isDebugMode()<br />
popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com0tag:blogger.com,1999:blog-8260844445527494264.post-25929731711698487332012-02-28T17:04:00.000+08:002012-02-28T17:44:10.712+08:00iOS blocks - 三個會造成retain cycle的anti patterns在iOS4.0推出了Blocks這個語言特性後<br />
到現在iOS都已經出到5.0了<br />
所以我想Blocks應該可以被廣泛應用了<br />
但現在iOS環境是從MRC(Manual Reference Counting) 走到ARC (Automatic Reference Counting)<br />
在Reference Counting的環境中Runtime是無法自動解除Retain cycle的<br />
而Blocks有很多隱性的retain的動作<br />
很容易不小心的造成retain cycle。<br />
而本篇的重點是點出三種會造成Retain Cycle的Anti-patterns<br />
再來講一下怎麼解決。<br />
<br />
在討論之前還是先大概重述一些概念<br />
block當中是允許去使用外部的variable<br />
但是local variable是會自動做retain的動作<br />
例如<br />
<pre class="code">MyClass* foo = ….;
self.someBlock = ^{
[foo bar];
};
</pre>上面的foo在此block被copy到heap的時候<br />
也會一起被自動retain<br />
而這就是我說的很容易造成retain cycle的主因。<br />
<br />
<br />
<b>Anti Pattern 1</b><br />
第一個例子我們先用大家很常用的Opne source library <a href="http://allseeing-i.com/ASIHTTPRequest/">ASIHttpRequest</a>當作一個範例<br />
看看下面的例子,有發現任何問題嗎?<br />
<pre class="code">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];
}];
</pre>這邊我們要首先要注意的點就是<code>[request setCompletionBlock:…]</code>這裡<br />
很明顯的這邊的用途是要做一個event callback的用途<br />
也就是說我給你一個block,當動作完成的時候callback我。<br />
這是一個典型的非同步的作法。<br />
但由於如果你要把block拿來之後使用,<br />
你一定要呼叫<code>[aBlock copy]</code>的動作,<br />
此動作會把block從stack丟進heap。<br />
因為在iOS的環境block也是一個object,<br />
此時這個block的retain count就會增加1<br />
這時候根據定義,這個block中參照的<code>request</code>這個local變數就也會被retain起來。<br />
所以<code>request</code>的retain count也會增加1。<br />
但問題來了,一旦<code>request</code>完成任務,應當要被release的時候<br />
卻會發現retain count始終無法歸零。理由是<br />
request <-> block 這兩個互相retain<br />
而無法正常釋放,這就是所謂的retain cycle了。<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgf7cXgOyyCmfGvWvXbjwseYij8oJPwWTh-4sOObe6hyphenhyphenzchXrE1b2IaJPgv3gLD_KWGS1_XZROyzRiJ9oH-ZKeWSrIvDavaudbuztI5hVkZmTEjfiJQBzdPlkasrvGNwjW9etgoRxpRtLU/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.45.55+PM.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="400" width="393" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgf7cXgOyyCmfGvWvXbjwseYij8oJPwWTh-4sOObe6hyphenhyphenzchXrE1b2IaJPgv3gLD_KWGS1_XZROyzRiJ9oH-ZKeWSrIvDavaudbuztI5hVkZmTEjfiJQBzdPlkasrvGNwjW9etgoRxpRtLU/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.45.55+PM.png" /></a></div><br />
解決方法很簡單,看看<a href="http://allseeing-i.com/ASIHTTPRequest/How-to-use#using_blocks">ASIHttpRequest官網的文件</a><br />
也就是用<code>__block</code>來描述<code>request</code><br />
<pre class="code"><font color="red">__block</font> 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];
}];
</pre>通過block variable不會retain的特性,<br />
有點類似weak reference的作用<br />
此時block就不會retain <code>request</code><br />
當然也就不會有retain cycle的問題。<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpg_BdX-ieiZmZfmhDydsdXGZi1QbeHuil0uHd-L5Kqub7z1Dht-xsIHRWz5rTFRWFs1Tssg_aQSED9BT0T6Zhkbtmb6ihQkYal4WGOlX6peszPLUzSwadOG2YxuBoiT-XebybbTx1oCY/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.46.11+PM.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="395" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpg_BdX-ieiZmZfmhDydsdXGZi1QbeHuil0uHd-L5Kqub7z1Dht-xsIHRWz5rTFRWFs1Tssg_aQSED9BT0T6Zhkbtmb6ihQkYal4WGOlX6peszPLUzSwadOG2YxuBoiT-XebybbTx1oCY/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.46.11+PM.png" /></a></div><br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0s1VG5ZNLOifxMJwQeYgcg5Mf2-NVTedHg-18LVIdZuxRQUFYPcEaAXpl9P-k51UYcyAWVGuef1gZsQinH3ZocR1SssDmg57cu7r66qFZUeRD7jFo5ENYNlg5ADRroQnZ_wFmDreDNVw/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.46.25+PM.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="208" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0s1VG5ZNLOifxMJwQeYgcg5Mf2-NVTedHg-18LVIdZuxRQUFYPcEaAXpl9P-k51UYcyAWVGuef1gZsQinH3ZocR1SssDmg57cu7r66qFZUeRD7jFo5ENYNlg5ADRroQnZ_wFmDreDNVw/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.46.25+PM.png" /></a></div><br />
<b>Anti Pattern 2</b><br />
Anti Pattern 1是在使用別人的library的時候容易出現的<br />
Anti Pattern 2是在實作自己的class的時候容易出現<br />
請看下面這段code<br />
<pre class="code">//MyClass.h
@property <nonatomic, copy> MyBlock onCompleteBlock;
//MyClass.c
self.onCompleteBlock = ^{
[self doSomething];
}
</pre>我相信這邊大家已經馬上看出問題在哪裡了<br />
其實Anti Pattern2算是Anti Pattern 1的特例<br />
只是這邊使用的是特殊變數self<br />
<br />
但有些時候我們更容易忽略的是在block中始用自己的member variable<br />
例如<br />
<pre class="code">//MyClass.h
@interface MyClass : NSObject
{
NSDate* lastModifed;
}
//MyClass.c
self.onCompleteBlock = ^{
lastModifed = [[NSDate date] retain];
}
</pre>這時候就沒有那麼容易察覺了。<br />
根據定義,在使用block的時候,<br />
如果我們使用到member variable,<br />
此時retain的不是lastModified指到的object<br />
而是retain <code>self</code>。<br />
所以造成的就是<br />
self <-> block 互相retain<br />
跟anti pattern 1一樣的結果就是無法最終釋放記憶體。<br />
<br />
這時候的解決方法也是一樣是拿出<code>__block</code>來用<br />
<pre class="code">//MyClass.c
<font color="red">__block</font> MyClass* tempSelf = self;
self.onCompleteBlock = ^{
tempSelf.lastModifed = [NSDate date];
}
</pre><br />
<b>Anti Pattern 3</b><br />
繼續看下面的code<br />
<pre class="code">SettingsViewController* settingsViewController =
[[[SettingsViewController alloc] init] autorelease];
settingsViewController.onUpdate = ^{
[self doUpdate];
}
self.settingsViewController = settingsViewController;
</pre>雖然這個Block中沒有直接使用到settingsViewController,感覺應該不會有retain cycle<br />
但是因為self -> settingsViewController<br />
而setttingsViewController -> block<br />
再來block -> self<br />
這就剛好繞了一圈,同樣會有retain cycle。<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgNUruIzCQJrQ4k81S3gWcqPb7bGmZY14H2JWKSuSUX1Y2FajOsyaFDuU-faIgND0uVAWW1Lk7PnZ2Huhn3kmX7uAjVCyimGYuiR9Tu-8o1_KxbKr89zSaUTGZUXSzLq_dfkZfXV1jh4j8/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.46.36+PM.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="343" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgNUruIzCQJrQ4k81S3gWcqPb7bGmZY14H2JWKSuSUX1Y2FajOsyaFDuU-faIgND0uVAWW1Lk7PnZ2Huhn3kmX7uAjVCyimGYuiR9Tu-8o1_KxbKr89zSaUTGZUXSzLq_dfkZfXV1jh4j8/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.46.36+PM.png" /></a></div><br />
所以呢,還是要想一樣用anti pattern 2的解法去解決<br />
<pre class="code">//RootViewController.m
SettingsViewController* settingsViewController =
[[[SettingsViewController alloc] init] autorelease];
<font color="red">__block</font> RootViewController* tempSelf = self;
settingsViewController.onUpdate = ^{
[tempSelf doUpdate];
}
self.settingsViewController = settingsViewController;
</pre><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjD9EnEbuwbBChnRgqApqQItLyXXpTEMckOeuRSg0oKXsSXeDCjjuY8sPB9gFd49ePdQVhCZ8JTFX6VX55IvBbpB0CYvuY9B3m_SJVLfVYOp5PRImobab2dkdKEv3EPSM-1AuxSyVdppa0/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.46.42+PM.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="337" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjD9EnEbuwbBChnRgqApqQItLyXXpTEMckOeuRSg0oKXsSXeDCjjuY8sPB9gFd49ePdQVhCZ8JTFX6VX55IvBbpB0CYvuY9B3m_SJVLfVYOp5PRImobab2dkdKEv3EPSM-1AuxSyVdppa0/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.46.42+PM.png" /></a></div><br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj9wgk9yIoOvSiIhgStlfYtHJSVUr5NzyzcQUoBjgk18jylox23dXpNzZpAW-lLIuZZcefO0GmipjQ-cVJAO_afxPs4AmQ3M-krU5YUUiV7teV_Q_y3S3VsH7_8cpTdz_8wDUmvM4QrT2U/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.46.51+PM.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="338" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj9wgk9yIoOvSiIhgStlfYtHJSVUr5NzyzcQUoBjgk18jylox23dXpNzZpAW-lLIuZZcefO0GmipjQ-cVJAO_afxPs4AmQ3M-krU5YUUiV7teV_Q_y3S3VsH7_8cpTdz_8wDUmvM4QrT2U/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2012-02-28+4.46.51+PM.png" /></a></div><br />
在reference counting的環境裡,<br />
我建議要解決retain cycle的最好思維就是<font color="red">想清楚從屬關係</font><br />
例如最後一個anti pattern<br />
他們的從屬關係應該就是<br />
RootViewController -> SettingsViewController -> block<br />
如果block要用到SettingsViewController或是RootViewController,<br />
則就要使用weak reference (也就是<code>__block</code>)<br />
在這樣的原則之下,就可以知道哪些要給他retain哪些不要了。<br />
<br />
最後要補充一點就是上面的例子都是在MRC環境下當做範例<br />
在MRC中<code>__block</code> variable在block中使用是不會retain的<br />
但是ARC中<code>__block</code>則是會Retain的。<br />
取而代之的是用<code>__weak</code>或是<code>__unsafe_unretained</code>來更精確的描述weak reference的目的<br />
其中前者只能在iOS5之後可以使用,但是比較好 (該物件release之後,此pointer會自動設成nil)<br />
而後者是ARC的環境下為了相容4.x的解決方案。<br />
所以上面的範例中<br />
<pre class="code">__block MyClass* temp = …; // MRC環境下使用
__weak MyClass* temp = …; // ARC但只支援iOS5.0以上的版本
__unsafe_retained MyClass* temp = …; //ARC且可以相容4.x以後的版本
</pre><br />
相關文章<br />
1. <a href="http://popcornylu.blogspot.com/2011/08/objective-c-block.html">探討Objective-C Block (part 1)</a><br />
2. <a href="http://popcornylu.blogspot.com/2011/08/objective-c-block-part-2.html">探討Objective-C Block (part 2)</a><br />
3. <a href="http://popcornylu.blogspot.com/2011/08/objective-c-block-part-3.html">探討Objective-C Block (part 3)</a>popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com0tag:blogger.com,1999:blog-8260844445527494264.post-51197388779542669532012-02-06T22:42:00.000+08:002012-02-06T22:57:40.417+08:00所見即所得再進化 - iOS5 StoryboardStoryBoard絕對是iOS5讓人眼睛一亮的功能<br />
在iOS5之前,我們已經可以透過Interface Builder中去編輯xib來設計View Controller的look & feel<br />
而StoryBoard的導入更可以直接的把整個WorkFlow都也一併的加入<br />
讓我們透過一個StoryBoard檔,就可以看得出整個應用程式的概括<br />
對於開發的效率絕對是有所助益。<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/SecondiOSAppTutorial/Art/after_modal_segue.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="482" width="648" src="https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/SecondiOSAppTutorial/Art/after_modal_segue.jpg" /></a></div><br />
然而在我開始接觸StoryBoard之前,<br />
我有幾個觀察重點<br />
1. 能不能夠相容於iOS5以下的版本<br />
2. 能不能完全取代.xib<br />
3. 用了Storyboard之後會不會有什麼樣的限制?<br />
4. 用了Storyboard之後,可否跟舊的.xib共存?<br />
先來跟大家解答,再逐一解釋,<br />
答案是1.No 2.Yes 3.No 4.Yes<br />
除了1的答案比較可惜以外,其他應該是很正面的答案<br />
<br />
1.的部分因為StoryBoard中多了幾個UIStoryboard的class,<br />
而UIViewController也增加新的UIStroyboard的method,property<br />
所以只能在iOS5之後使用,<br />
這點我認為是唯一現在不開始用Storyboard的考量,其他都不是太大的問題。<br />
<br />
2. 能完全取代.xib!! 事實上.storyboard可以說是.xib的集合,<br />
並且又增加了view controller之間的關聯 (segue)<br />
所以我認為我們可以直接透過.storyboard來做所有xib的事情,<br />
並且集中在一個storyboard絕對是個好方法。<br />
<br />
3. App中始用了Storyboard之後不會有什麼增加的限制。<br />
雖然看起來Storyboard好像會把ViewController到另一個ViewController中的行為定的死死的,<br />
但事實上不然。<br />
<br />
首先,對於非常死的動作當然用Storyboard可以定義到說按下某個button之後,<br />
navigate到哪個ViewController這種行為可以透過拖拉就完成,<br />
不需要去寫code。<br />
<br />
再來,如果是說要做到conditional action的動作。<br />
例如可能想要做到說按下按鈕,在某種condition才會去做這個segue的動作。<br />
這也是OK的,<br />
因為我們還是可以透過UIViewController的<code>-[UIViewController performSegueWithIdentifier:sender:]</code>去用程式的方法去執行你的segue。<br />
也就是說在這個需求的情況下,<br />
我們應該是要自己去實作這個event的action<br />
並且在你的情況成立時,再去透過上述的方法去執行你的segue。<br />
<br />
再來就是segue也沒有一定要綁在某個UIControl上面,<br />
我們可以直接定義一個UIViewController到另外一個UIViewController的segue<br />
方法是直接在UIViewController上面control+click拖曳到另外一個UIViewController,<br />
就可以新增一個segue<br />
之後也是一樣是透過<code>-[UIViewController performSegueWithIdentifier:sender:]</code>去執行你的segue..<br />
<br />
最後,你如果不想用segue的方法去瀏覽,<br />
你還是可以用最傳統的方式,或是用xib的方式,產生你的UIViewController<br />
再透過push或是modal的方式瀏覽你的view controller。<br />
<br />
以上可知,用了storyboard只是把一些常用的動作簡化。但是複雜的行為還是可以用舊的方法達到目的<br />
<br />
4. Storyboard可以跟xib並存是肯定的。甚至你的application bundle可以有多個.storyboard跟.xib並存都沒問題<br />
我們可以透過<code>+[UIStoryboard storyboardWithName:bundle:]</code>去取得Storyboard的instance<br />
再透過這個instance來產生initial view controller或是某個identifier的view controller,<br />
可以視你的程式所需要的模組化需求來決定哪些要放在同一個Storyboard當中。<br />
例如我可以把所有設定相關的部分放在Settings.storyboard當中,<br />
而其他應用程式Workflow放在MainStoryboard.storyboard。<br />
至於很獨立的UIViewController,則單獨定義一個.xib檔<br />
並且透過<code>-[UIViewController initWithNibName:bundle:]</code>來產生。<br />
<br />
另外說一下我覺得用Storyboard來取代xib的優點。<br />
由於我們現在可以把UITabViewController, UINavigationController,<br />
還有我們的所有自訂的ViewController連在一起。<br />
以往我們要拖拉元件時,要手動告知Interface Builder說我的ViewController有navigation bar,有tab bar,<br />
現在因為多了這些關聯變成一切更自動了。<br />
<br />
以下面圖中的例子<br />
原本是沒有UINaviationController的<br />
我們就會看到沒有Navigation bar<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_VPF0R74_scOxtlN6hKNllGPY_266qBrDIxdjQ4OYuA5uZiXcS7A3w3gY0wWPLr0lNPjAnkahXD3BwOr4zY2vinxbMtgWqJr22UHqQR_V3_RJXYmq4Bp16JITf9uVUyOF7rKpIdCS90Q/s1600/storyboard1.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="283" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_VPF0R74_scOxtlN6hKNllGPY_266qBrDIxdjQ4OYuA5uZiXcS7A3w3gY0wWPLr0lNPjAnkahXD3BwOr4zY2vinxbMtgWqJr22UHqQR_V3_RJXYmq4Bp16JITf9uVUyOF7rKpIdCS90Q/s400/storyboard1.png" /></a></div><br />
如果我加上了UINavigationController<br />
就會多出了NavigationBar<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpfQuzAXn2eihkTGmPudHjBGcBlNBuu17M8QMvDa0QBXo1QT4yz1I7XgtTAHYbuc0-4ccX2Ql6JYJnItIGz3iYmbPDwACdiiQ9e1mPGTbpELx-itB1c87viYhztv3m7uuRSiRQErYha5w/s1600/Storyboard2.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="272" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpfQuzAXn2eihkTGmPudHjBGcBlNBuu17M8QMvDa0QBXo1QT4yz1I7XgtTAHYbuc0-4ccX2Ql6JYJnItIGz3iYmbPDwACdiiQ9e1mPGTbpELx-itB1c87viYhztv3m7uuRSiRQErYha5w/s400/Storyboard2.png" /></a></div><br />
如果我再加上UITabViewController<br />
則TabBar也會自動跑出來,<br />
這也算是用了Storyboard才會有的好處。<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgaM0iM8PcQTRAei1xINGTT9YhakHtZgeJqk1qgyMHHZtdQ8QiXBLw8DrjCLkcYtcvNgIYlPSsaePxNV9MgslehT0brGOXbNBLzcVit9zdaPWQUzebY4AW3Fa2VDFUDBnQGi63hHjyG1gY/s1600/Storyboard3.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="171" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgaM0iM8PcQTRAei1xINGTT9YhakHtZgeJqk1qgyMHHZtdQ8QiXBLw8DrjCLkcYtcvNgIYlPSsaePxNV9MgslehT0brGOXbNBLzcVit9zdaPWQUzebY4AW3Fa2VDFUDBnQGi63hHjyG1gY/s400/Storyboard3.png" /></a></div><br />
我的結論是<br />
如果你開發的app沒有要支援iOS5以下的target<br />
那我會毫無考慮的建議你就直接用Storyboard吧!!!<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com2tag:blogger.com,1999:blog-8260844445527494264.post-39039132323575104212012-02-03T21:51:00.000+08:002012-02-06T22:57:54.879+08:00Appcelerator Titanium一日遊<div class="separator" style="clear: both; text-align: center;"><a href="http://developer.appcelerator.com/assets/img/DOCS_big_image.png" imageanchor="1" style="clear:right; float:right; margin-left:1em; margin-bottom:1em"><img border="0" height="191" width="276" src="http://developer.appcelerator.com/assets/img/DOCS_big_image.png" /></a></div>因為前一個project告一段落<br />
而在下個project進來之前剛好有一些空擋<br />
所以最近比較可以研究一下其他東西<br />
今天就來玩玩前一陣子聽別人在用的PhoneGap或是Titanium這種cross-platform mobile application solution。<br />
在網路上搜尋了一下<br />
看到了這篇討論<br />
<a href="http://stackoverflow.com/questions/1482586/comparison-between-corona-phonegap-titanium">Comparison between Corona, Phonegap, Titanium</a><br />
細節我不說了<br />
結論大概是PhoneGap支援比較多的平台<br />
而Titanium有比較好的native UI整合<br />
由於我希望讓使用者看不太出來是非native sdk寫出來的<br />
所以我就選擇來玩玩<a href="http://www.appcelerator.com/">Titanium</a>!!<br />
下面是我的"一日"心得報告,可能有所偏頗,所以大家參考參考就好,<br />
如有錯誤也歡迎指教。<br />
<br />
首先到<a href="http://www.appcelerator.com/products/download/">這邊來下載Titanium SDK</a><br />
目前我抓到最新版本是1.8.1<br />
主要支援的是iOS跟Android目前最主流的這兩個mobile平台<br />
Titanium主要是用Javascript去撰寫所有的app<br />
No Java, No Objective-C, just only javascript!!!! <br />
這對於從Web programming過來的programmer應該是超級福音<br />
但是需要注意的是.. 你還是需要安裝Android SDK跟iOS sdk<br />
也就是說如果你要跑iOS<br />
你還是需要一台mac XD<br />
不過那本來對我就不是個問題<br />
我想要的只是希望Write once, and iOS and android version runnable..<br />
<br />
再來就是開發環境<br />
如果你以前已經有用過eclipse那你應該可以很熟悉這個環境<br />
因為Titanium所用的開發環境TitaniumStudio就是用eclipse改過來的<br />
所以開發環境非常的成熟而且整合的不錯<br />
<br />
至於開發到底容易不容呢?<br />
我只花了一天的時間就寫出了一個簡單的GPS定位自己的位置,<br />
並且把google map秀出自己附近的地圖 真的不是很難寫..<br />
但是強烈建議,把<a href="http://training.appcelerator.com/zero-to-app">官方影片</a>從101到104先看過一遍<br />
再來把幾個sample project抓下來跑跑看,看一下他的程式架構<br />
絕對大大的加快你熟悉開發環境的速度<br />
<br />
經過一天的評估<br />
我覺得如果將來有需要跨iOS跟Andriod的平台需求<br />
我會很想要用Titanium去實作看看<br />
畢竟要同時開發兩個平台的成本可能是1+1 > 2<br />
通常也至少要兩個人才有辦法<br />
而且同時會iOS跟Andriod的人才太少了<br />
而Titanium只要一個code base就可以搞定兩個平台了<br />
這點就太吸引人了<br />
<br />
不過會讓我比較擔心的是網路上有人提到此平台還為數不少的bug<br />
然後再來就是畢竟多包了一層<br />
所以平台的掌握度也跟直接用原生SDK不能比<br />
至於值不值得<br />
等到我真的有用Titanium完成一個專案後再來分享吧 :D<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com1tag:blogger.com,1999:blog-8260844445527494264.post-54239314227110842122012-02-01T17:56:00.000+08:002012-02-08T10:39:12.308+08:00Push Notification簡介上一篇文章的<a href="http://popcornylu.blogspot.com/2012/01/push-notification-step-by-step.html">push notification - step by step</a>主要是以實作來去撰寫<br />
但想說網路上中文的push notification的文章真的很少<br />
那我就再來寫一篇來介紹ios push notification<br />
<br />
push notification主要由三方溝通<br />
也就是iOS device, provider, APNS (apple push notification service)<br />
其中iOS device中又可以把iOS系統層跟application分開來<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjfCU2Q9tCy7naDMGFYGVjV5d6sR9UNsS5ssLV50ttwjtHWYI2phWgXFG8-frF3d80nAiwzShErcWIGVjyzcjf9FXwvmWdVq83W4YpyLvYU8tVMXl_e3ORb0zGhYGNxlM0P8e7p424tQGw/s1600/Screen+Shot+2012-02-01+at+5.54.49+PM.png" imageanchor="1" style=""><img border="0" height="340" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjfCU2Q9tCy7naDMGFYGVjV5d6sR9UNsS5ssLV50ttwjtHWYI2phWgXFG8-frF3d80nAiwzShErcWIGVjyzcjf9FXwvmWdVq83W4YpyLvYU8tVMXl_e3ORb0zGhYGNxlM0P8e7p424tQGw/s400/Screen+Shot+2012-02-01+at+5.54.49+PM.png" /></a></div><br />
<br />
1. iOS跟APNS的SSL連線<br />
這個連線是我們開發者不會直接接觸的,<br />
是iOS系統層跟APNS索建立的連線<br />
透過這個唯一的連線<br />
iOS系統可以統一的處理所有application跟apns中間的溝通<br />
裡面走的是jabber/xmpp的protocol, APNS port是5223<br />
<br />
2. provider跟APNS的SSL連線<br />
這個連線是讓provider server去通知APNS去notify device用的<br />
這個SSL certification key是要在iOS provision portal在app id那邊產生的<br />
裡面走的protocol是apple定義的一個binary protocol<br />
又分simple跟advanced兩種格式<br />
詳見 <a href="http://goo.gl/2EB9J">http://goo.gl/2EB9J</a><br />
而APNS port是2195<br />
<br />
3. Application在啓動的時候需要呼叫<code>- [UIApplication registerForRemoteNotificationTypes:]</code><br />
來去跟iOS註冊要使用push notification<br />
如果註冊成功的話,iOS會回呼<code> -[UIApplicationDelegate application:didRegisterForRemoteNotificationsWithDeviceToken:]</code><br />
此時可以取得一個32bytes的device token<br />
<br />
4. 當第三步的registration成功後,application需要跟provider告知我的device token是什麼。<br />
因為這個是屬於application developer的自訂邏輯,<br />
怎麼樣跟provider server告知這個device token完全看自己的需求設計<br />
而provider要做的就是要把iOS傳來的device token放進provider的DB當中,<br />
以便當有通知要觸發的時候,可透過provider<->APNS的connection來觸發通知的動作<br />
<br />
<br />
至於當Device收到push notification之後會發生什麼事情?<br />
如果App是在還沒被啟動的狀況下被呼叫,則device會出現類似下面的畫面。<br />
使用者如果選擇執行這個notification,<br />
則你的應用程式會被打開<br />
並且呼叫 <code>- [UIApplicationDelegate application:didFinishLaunchingWithOptions:]</code><br />
透過第二個參數的地方可取得此notification的資料<br />
<br />
相對的,如果我們的App在notification來之前已經是在執行的狀態了<br />
則底層會改呼叫<code>- [UIApplicationDelegate application:didReceiveRemoteNotification:]</code><br />
<br />
<br />
push notification的packet可以做的事情不多<br />
裡面只能放alert body, badge number, sound, launch image, 以及custom defined key/value<br />
而這些資料要以json格式描述並且要在255 bytes之中… <br />
但最重要的是,它提供了一個完美機制讓我們的應用程式即使沒有在運作,<br />
但透過遠端的觸發,讓事件發生時,可以通知使用者喚醒你的App。<br />
<br />
<br />
<b>Reference</b><br />
[1] <a href="http://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008194">Local and Push Notification Programming Guide</a><br />
[2] <a href="https://developer.apple.com/appstore/push-notifications/index.html">Using Local and Push Notifications</a><br />popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com5tag:blogger.com,1999:blog-8260844445527494264.post-87189236100563922542012-01-20T23:13:00.001+08:002012-01-20T23:13:30.599+08:00push notification - step by step所謂萬事起頭難<br />
Push notification更是如此<br />
由於Push notification因為牽扯到iOS app, provider server跟APNS三方溝通<br />
provider server又跟APNS中間溝通要透過SSL<br />
再加上有些東西需要在provisioning portal做設定<br />
所以我覺得對初學者來講第一步真的很困難的<br />
<br />
最近在搞這個,我就把我實驗出可以work的方法記錄下來<br />
除了當做自己的備忘以外<br />
也同時提供也有同樣需求的做參考<br />
<br />
主要有幾個步驟,測試請一定要在device上做,push notification無法在模擬器上模擬<br />
1. 在xcode新增一個App,並且在AppDelegate加上以下的code<br />
<pre class="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];
<font color="red"> [[UIApplication sharedApplication] registerForRemoteNotificationTypes:UIRemoteNotificationTypeAlert];
return YES;</font>
}
<font color="red">- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
NSLog(@"receive deviceToken: %@", deviceToken);
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
NSLog(@"Remote notification error:%@", [error localizedDescription]);
}</font>
</pre><br />
先來執行看看..<br />
當然,不可能這樣就ok。你應該會在Debug訊息中得到這個error<br />
<code>Remote notification error:找不到應用程式的有效“aps-environment”授權字串</code><br />
不用擔心,那是因為你還有些事情還沒做<br />
<br />
2. <br />
到iOS Provisioning Portal, <br />
首先你要先產生一個app id<br />
而這個app id的bundle id要跟你前面產生的app的bundle id一致。<br />
(所謂的bundle id就是類似com.example.APNTest)<br />
<br />
3.<br />
產生完app id之後,點進這個app id<br />
我們可以看到這個畫面..<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaaak4LEbaSsYdvj1NXwqpfmEdqveWSODk9MgPvSfv37OeJWnWbU7i8wKAdXG6dlaanjcpbMMikH4vKGnZmWRbAvyg1y9Pl8WlU32ebEEqKnklW615IUOjA_g-W-qr5fpp-ZgE1QSMrqs/s1600/AppID_CRS1.png" imageanchor="1" style=""><img border="0" height="97" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaaak4LEbaSsYdvj1NXwqpfmEdqveWSODk9MgPvSfv37OeJWnWbU7i8wKAdXG6dlaanjcpbMMikH4vKGnZmWRbAvyg1y9Pl8WlU32ebEEqKnklW615IUOjA_g-W-qr5fpp-ZgE1QSMrqs/s400/AppID_CRS1.png" /></a></div>請打開push notification的功能。<br />
因為我們是測試,所以先以development為例。<br />
按下"Development Push SSL Certificate"的Configure按鈕<br />
此時會看到下面的畫面,按照裡面的動作產生Certificate Signing Request<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjv7BLLpnSfXiWZxQrjdlyZMz_e2vldPuR3YWDgeoihziDK_KBnCATIcdOnRrXNtm8o1rJJLbQUPZ-NKZ72CQl2ECTTagAdwf1Vkh1eSpYGW1jrv2ILnfkrKSkjMN5Nj7eP8cOQS87dUb4/s1600/AppID_CSR2.png" imageanchor="1" style=""><img border="0" height="367" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjv7BLLpnSfXiWZxQrjdlyZMz_e2vldPuR3YWDgeoihziDK_KBnCATIcdOnRrXNtm8o1rJJLbQUPZ-NKZ72CQl2ECTTagAdwf1Vkh1eSpYGW1jrv2ILnfkrKSkjMN5Nj7eP8cOQS87dUb4/s400/AppID_CSR2.png" /></a></div><br />
<br />
他告訴我們要使用keychain access這個app<br />
並且在選單上找到這個選項<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVWHnxH0Wos1_tgjI78dGYyi6BSsExw0Rlahyphenhyphen9WVfErhtn4pSXUFs9TnZqQqn6Sct400Z0BnXM2UqCiRxoPLT3ppUGQQWaohtE8NWoDTf5WKEbFz8NobRy3ZkbPRaWryV-oPVqhhyg1uc/s1600/Keychain_CSR1.png" imageanchor="1" style=""><img border="0" height="127" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVWHnxH0Wos1_tgjI78dGYyi6BSsExw0Rlahyphenhyphen9WVfErhtn4pSXUFs9TnZqQqn6Sct400Z0BnXM2UqCiRxoPLT3ppUGQQWaohtE8NWoDTf5WKEbFz8NobRy3ZkbPRaWryV-oPVqhhyg1uc/s400/Keychain_CSR1.png" /></a></div><br />
點了之後,輸入你的email跟certification name<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7GRiIRsZpMugjj2NBU6-wuMZezRzD_wsCGQTdFL_EgcAm4yx76SHGnSy4WtbtUXGx7uNC0Ty12RO1h0zjuyvwSCQRZCBDzerdhZjpwj43CCZB16gL4MTMo_cGomWhr4uJnWY05SoqN9Y/s1600/Keychain_CSR2.png" imageanchor="1" style=""><img border="0" height="301" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7GRiIRsZpMugjj2NBU6-wuMZezRzD_wsCGQTdFL_EgcAm4yx76SHGnSy4WtbtUXGx7uNC0Ty12RO1h0zjuyvwSCQRZCBDzerdhZjpwj43CCZB16gL4MTMo_cGomWhr4uJnWY05SoqN9Y/s400/Keychain_CSR2.png" /></a></div><br />
產生之後save to disk,並且回到ios provisioning portal<br />
按continue之後會看到這個畫面<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhu4wJ0Y6fTn-X39rAX47dvSy3WwZIkG6nVXqEaOeRhtUfXpLI1aFoYn8XkWYphX0uN0er0qwYy5emBM6SuhYL6WPKSaNzbtZcI1utXNF7K4Qtb68jasyRyYYCTALB9uYkDliDKvTh5aUY/s1600/AppID_CSR3.png" imageanchor="1" style=""><img border="0" height="367" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhu4wJ0Y6fTn-X39rAX47dvSy3WwZIkG6nVXqEaOeRhtUfXpLI1aFoYn8XkWYphX0uN0er0qwYy5emBM6SuhYL6WPKSaNzbtZcI1utXNF7K4Qtb68jasyRyYYCTALB9uYkDliDKvTh5aUY/s400/AppID_CSR3.png" /></a></div><br />
指定剛剛的CSR則可以得到這個畫面<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGe6Abr0BPnmIWelSpR-wwIQ19CU1N_lWw26BHm9OumkPNjUpX68szjPmIGT3-v0PFAMZl_lUgC2bvxcDG3crHTYFGevai_urheG-pte1oW5gEO_suyIfZT9uCA5ogAgInX634o5dH0H0/s1600/Keychain_CSR3.png" imageanchor="1" style=""><img border="0" height="366" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGe6Abr0BPnmIWelSpR-wwIQ19CU1N_lWw26BHm9OumkPNjUpX68szjPmIGT3-v0PFAMZl_lUgC2bvxcDG3crHTYFGevai_urheG-pte1oW5gEO_suyIfZT9uCA5ogAgInX634o5dH0H0/s400/Keychain_CSR3.png" /></a></div><br />
之後就把這個Certification抓下來<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjxYjX853IFTbZpS1qanq3XC-9YsOg3f2l8ldXgChpXMlDVPLyF6GuLazc5qTILkQg1uJAQk0TDoLMWqHg3aBKctBDVFrN4Gh03gEFUPr8S1MQMWK057JsND30YR8T6DZG5dF_giG2dO6A/s1600/Keychain_CSR4.png" imageanchor="1" style=""><img border="0" height="367" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjxYjX853IFTbZpS1qanq3XC-9YsOg3f2l8ldXgChpXMlDVPLyF6GuLazc5qTILkQg1uJAQk0TDoLMWqHg3aBKctBDVFrN4Gh03gEFUPr8S1MQMWK057JsND30YR8T6DZG5dF_giG2dO6A/s400/Keychain_CSR4.png" /></a></div><br />
抓下來記得點兩下安裝,這時候你的Keychain access應該會出現新的Cert.跟Private Key<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjd5hxkjFuBCoxzUe4o5MAs-aQJZBlHNOwc0jS4uzRBkaRAQDX5Yw4tNL9CRgq9oNoIXzaMPJTsbJR6ePHaxLbyqdpg4PKiok5O1OALvMfvBLv-NxkC_KKr2O6hgpuo1cxparg2GqfOFnQ/s1600/Keychain_CSR5.png" imageanchor="1" style=""><img border="0" height="370" width="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjd5hxkjFuBCoxzUe4o5MAs-aQJZBlHNOwc0jS4uzRBkaRAQDX5Yw4tNL9CRgq9oNoIXzaMPJTsbJR6ePHaxLbyqdpg4PKiok5O1OALvMfvBLv-NxkC_KKr2O6hgpuo1cxparg2GqfOFnQ/s400/Keychain_CSR5.png" /></a></div><br />
4. <br />
好了之後,回到iOS provisioning portal<br />
切到Provisioning的Development,按下New Profile<br />
產生一個新的Provision Profile是對應到你的bundle id的<br />
產生完之後,抓下來點兩下,就可以安裝到你的xcode<br />
<br />
5. <br />
回到XCode<br />
重新run你的app<br />
應該就會成功的取得device token<br />
receive deviceToken: <c87c312f 1efc...><br />
如果還是不行,請手動指定你的provision profile到你剛剛產生download下來的那個profile..<br />
<br />
6.<br />
上面是已經成功的把device跟app註冊到APNS了<br />
接下來我們要做的是去讓Server上的程式去notify我們的app<br />
因為要用SSL<br />
我們要先產生p12檔才可以建立SSL<br />
回到keychain access<br />
選擇你剛剛的certification按右鍵<br />
選擇export就可以輸出成p12了<br />
<br />
7.<br />
由於跟provider server跟APNS溝通的部分跟iOS無關<br />
所以我寫了一個簡單的java程式去測試<br />
細節就不講了<br />
大家可以從<a href="http://dl.dropbox.com/u/12450729/APNS/APNTest_java.zip">這邊</a>抓下來去使用<br />
解開後,把剛剛輸出的.p12檔也放在該目錄之中<br />
打開Console,到這個解開的目錄<br />
並且打下下面的command<br />
<pre class="code">java -cp apns-0.1.5.jar:. APNTest "APNTest.p12" "APNTest" "c87c..." "Hello APN"</pre>最後四個參數分別是<br />
(1) 你的.p12的檔名<br />
(2) 你的.p12的密碼<br />
(3) 你的device token。從你上面印出來的token除掉中間空格就是了<br />
(4) 你要送的message。<br />
<br />
如果你看到下面的畫面<br />
恭喜你,你踏出push notification的第一步了<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgOS5HJgRz4cUe2qjw0PkFQ7MYyPtt0WGFZtbGwcam9qCPlkRtl5Amph2yXBoMkE693Z-_mPcOY8u1fiy1oi1GVUbo0QabAj7tMy_M2DLZBiDqUouz01RjE-KrEBMaJTgNAHkDjxCj8LCc/s1600/result.PNG" imageanchor="1" style=""><img border="0" height="400" width="267" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgOS5HJgRz4cUe2qjw0PkFQ7MYyPtt0WGFZtbGwcam9qCPlkRtl5Amph2yXBoMkE693Z-_mPcOY8u1fiy1oi1GVUbo0QabAj7tMy_M2DLZBiDqUouz01RjE-KrEBMaJTgNAHkDjxCj8LCc/s400/result.PNG" /></a></div><br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com8tag:blogger.com,1999:blog-8260844445527494264.post-66164533081899443802011-12-23T16:36:00.000+08:002011-12-23T16:37:59.009+08:00Simple template engine for objective c所謂template engine就是我們可以先定義一個template<br />
例如一個html檔案<br />
然後我們會希望把裡面少部分的片段透過程式去取代成我們想要的字串<br />
而做這些取代動作的東西我們就稱作template engine<br />
<br />
其實在iOS當中已經有一個非常簡單的template engine了,<br />
也就是<code>+[NSString stringWithFormat:(NSString *)format, …]</code><br />
這東西就很像在c使用<code>sprintf(...)</code><br />
然而,這東西對大部份來時候來講都很好用,但是也有一些限制,<br />
例如它必需要要一個蘿蔔一個坑 (傳進的參數就是對應到format中的每個%開頭的取代標示)<br />
你不能要求一個蘿蔔兩個坑 (一個參數取代兩個取代標示)<br />
甚至第一個蘿蔔一定要放在第一個坑 (第一個參數一定要放在第一個取代標示)<br />
<br />
我查了一下stackoverflow<br />
但是沒有找到我想要的答案<br />
<a href="http://stackoverflow.com/questions/2539556/objectivec-builtin-template-system">http://stackoverflow.com/questions/2539556/objectivec-builtin-template-system</a><br />
得到的答案不外乎就是這幾個<br />
1. 用上面提的<code>+[NSString stringWithFormat:(NSString *)format, …]</code><br />
2. 用<code>-[NSString stringByReplacingOccurrencesOfString:withString:]</code><br />
然後一個一個把取代標示換成我們要的字串<br />
3. 不然就是給我一個非常強大的template engine..<br />
<br />
其實這東西不難做,既然我們都已經有好用的<code>NSScanner</code>了,那我們就做一個簡單的template engine吧。<br />
使用上我希望長這樣<br />
<pre class="code">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."
</pre><br />
程式碼如下<br />
<script src="https://gist.github.com/1513528.js?file=NSString+template.c"></script><br />
<br />
<br />popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com1tag:blogger.com,1999:blog-8260844445527494264.post-25803336934738127912011-10-31T10:17:00.000+08:002011-10-31T13:46:21.678+08:00如何把Target/Action放進array裡面呢?Target-Action相信這個design pattern對於寫ios一陣子的人都不陌生<br />
這是<a href="http://developer.apple.com/library/ios/#documentation/iphone/conceptual/iPhone101/Articles/02_DesignPatterns.html">幾個ios fundamental design pattern之一</a><br />
然而有時候我們會希望把幾個target action放進array或是dictionary之中,或是直接當作member variable<br />
但是SEL並不是一般繼承於NSObject的class,所以不能直接放進Array當中<br />
當然我們可以用<br />
<code>NSStringFromSelector(SEL aSelector)</code><br />
以及<br />
<code>NSSelectorFromString(NSString *aSelectorName)</code><br />
讓SEL可以跟NSString*互換<br />
但是怎麼都覺得不是很方便<br />
我就在想有沒有比較漂亮的方法<br />
<br />
我找了一下iOS foundation library,當中就屬<code><a href="http://developer.apple.com/library/ios/#documentation/Cocoa/Reference/Foundation/Classes/NSInvocation_Class/Reference/Reference.html">NSInvocation</a></code>最合適<br />
這個Class就是把target, action, arguments包在一起,<br />
當我們呼叫<code>-[NSInvocation invoke]</code><br />
就會呼叫這個target的selector,並且傳進這些arguments... <br />
太棒了,這就是我要的class!!<br />
<br />
但是NSInvocation的產生方式實在不怎麼好用,如果我們要把這些東西全部丟進去<br />
那我們需要如下面這段code<br />
<pre class="code">NSMethodSignature* sig =
[[target class] instanceMethodSignatureForSelector:@selector(mySelector:];
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:target];
[invocation setSelector:selector];
</pre>看起來很不友善吧...<br />
<br />
不好用的話,那我們就來自己動手做Category吧...<br />
<br />
NSInvocation+PopcornyLu.h / NSInvocation+PopcornyLu.m<br />
<script src="https://gist.github.com/1321980.js"> </script><br />
<br />
這邊我提供了兩個Class messages來產生NSInvocation<br />
其中第二個可以提供不定量的arguments.<br />
使用上很簡單<br />
<pre class="code">// Put invocations in array
NSArray* myActions =
[NSArray arrayWithObjects:
[NSInvocation invocationWithTarget:self
selector:@selector(action1)],
[NSInvocation invocationWithTarget:self
selector:@selector(action2:)
arguments:@"foo", nil],
[NSInvocation invocationWithTarget:self
selector:@selector(action3WithFoo:andBar:)
arguments:@"foo", @"bar", nil],
nil];
// invoke all the actions
for(NSInvocation* invocation in myActions)
{
[invocation invoke];
}
</pre><br />
[補充]<br />
當然也可以用block來取代target/action<br />
以上面的例子我們可以用block來實作<br />
<pre class="code">NSArray* myActions2 =
[NSArray arrayWithObjects:
[[^(void){
[self action1];
} copy] autorelease],
[[^(void){
[self action2:@"foo"];
} copy] autorelease],
[[^(void){
[self action3WithFoo:@"foo" andBar:@"bar"];
} copy] autorelease],
nil];
// invoke all the actions
for(void(^invocation)() in myActions2)
{
invocation();
}
</pre><br />
但是缺點是code比較凌亂一點,然後action1, action2:, action3WithFoo:withBar這些message都要在前面要先定義<br />
稍微麻煩了點<br />
但基本上Target/Action或是Block都有其方便之處<br />
像這邊我希望array creation的地方不要寫太多code,所以我會選擇用target/action<br />
但有時候使用上會用inline block比較方便,那我就用block<br />
<br />
<br />
<br />popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com1tag:blogger.com,1999:blog-8260844445527494264.post-77636800053196845122011-09-21T17:37:00.002+08:002011-11-03T10:27:54.986+08:00如何實作通知使用者有version update可能有一種需求是我們希望如果appstore上有新版的軟體上架<br />
我們會要通知使用者是否要更新<br />
這時候我們可以透過這個URL去取得你的app資訊<br />
<a href="http://itunes.apple.com/lookup?id=441252681">http://itunes.apple.com/lookup?id=appid</a><br />
回傳的結果可能是這樣.<br />
中間就有版本號碼啦<br />
不要問我怎麼抓怎麼parse json<br />
這個solution很好找的<br />
<pre class="code">{
"resultCount": 1,
"results": [
{
"kind": "software",
"artistId": 291728775,
"artistName": "5Knot",
"price": 0,
"version": "1.1",
"description": "90 Minutes .....",
"genreIds": [
"6005",
"6012"
]
}
]
}
</pre>然後自己local的版本可以這樣拿<br />
<pre class="code">[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]
</pre>自己再寫一個NSString的category去比較版本 <pre class="code">@implementation NSString(VersionCompare)
- (NSComparisonResult)compareVersion:(NSString *)version
{
NSArray *thisVersionParts = [self componentsSeparatedByString:@"."];
NSArray *versionParts = [version componentsSeparatedByString:@"."];
NSUInteger count = MIN([thisVersionParts count], [versionParts count]);
NSUInteger i;
for (i = 0; i < count; i++)
{
NSUInteger thisPart = [[thisVersionParts objectAtIndex:i] integerValue];
NSUInteger part = [[versionParts objectAtIndex:i] integerValue];
if (thisPart > part)
{
return NSOrderedDescending; //version is older
}
else if (thisPart < part)
{
return NSOrderedAscending; //version is newer
}
}
if ([thisVersionParts count] > [versionParts count])
{
return NSOrderedDescending; //version is older
}
else if ([thisVersionParts count] < [versionParts count])
{
return NSOrderedAscending; //version is newer
}
return NSOrderedSame; //version is the same
}
</pre>
搞定!!<br>
<br>
<b>Rerefenace</b><br>
[1] <a href="http://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html">Official search API</a><br>
[2] <a href="https://github.com/nicklockwood/iVersion">iVersion</a><br>
<br />
<br />popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com1tag:blogger.com,1999:blog-8260844445527494264.post-27437565027229188192011-09-21T10:04:00.000+08:002013-04-12T18:00:01.955+08:00In App Purchase的實作心得分享其實IAP的一些概念跟使用方法已經有嘎李羊的詳細介紹了,在此貼上連結,但就不再重複分享了 <br />
1. <a href="http://iamgarlic.blogspot.com/2011/04/ios-in-app-purchase-1.html">iOS In App Purchase 學習筆記 (1)</a><br />
2. <a href="http://iamgarlic.blogspot.com/2011/04/ios-in-app-purchase-2.html">iOS In App Purchase 學習筆記 (2)</a><br />
3. <a href="http://iamgarlic.blogspot.com/2011/07/ios-in-app-purchase-3-app-store.html">iOS In App Purchase 學習筆記 (3) : 如何從App Store取得商品資訊</a><br />
<br />
我這邊就補充一下我最近研究IAP的心得,我就分別以三個類型Consumable, Non-consumable, Auto-renewable subscription來分別分享<br />
<b>Consumable</b><br />
1. 可以透過此來購買Credit(虛擬貨幣),但是必須要在你的app中消費掉,不能拿來購買app以外的商品或是實體商品。<br />
2. 通常實作的方法是交易成功後,程式自己增加購買的點數,這邊的邏輯StoreKit並不會負責幫你做credit增加的部份。<br />
3. 雖然Consumable product可以設定quantity,來一次購買多量的商品,但是我建議還是不要這麼做。你可以用陳列多個商品來達到一次買多量credit的目的。例如:<br />
10credits $0.99<br />
25credits $1.99<br />
50credits $2.99<br />
4. 如果要讓多個device share你的credit好像沒有比較好的方法,因為我們無法取得apple id,所以不知道怎麼share。<br />
而如果把credit存在server,那會不會違背policy這我就不敢說了。<br />
<br />
<b>Non-consumble</b><br />
1. 買了之後就永久有效。即使app移除了再裝回來,或是在別的device裝app,只要用同個apple id就可以restore。<br />
2. 有關restore相關的文章請看 <a href="http://developer.apple.com/library/IOS/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/MakingaPurchase/MakingaPurchase.html#//apple_ref/doc/uid/TP40008267-CH3-SW2">Restoring Transactions</a><br />
建議在你的app中如果有支援Non-consumable的app,要提供restore的功能,讓使用者有辦法恢復購買。<br />
當然用原本的buy的方法也可以restore,但是比較不user friendly。<br />
3. 交易成功後,記得要把買成功的狀態儲存起來,而app也要對應的打開對應的功能給end-user。<br />
<br />
<b>Auto renewable subscription</b><br />
1. 有點類似Non-consumable product,但是是有時效性的服務。當訂閱週期到了,app store會自動展期,除非使用者取消自動展期。<br />
2. 在itunes connect上面我們可以設定多種duration<br />
3. 使用者可以透過[設定]這個app中的 Store -> Apple ID: -? -> 檢視Apple ID -> Manage Subscriptions 來取消訂閱或是更改週期<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSOdYKCKPHuCFt-h712XGoLqILkMA_YvPee2BiaMDNVJ-CS-nGzGFhLHOQilsIsE4JrdLYQ0blBAV9atNymlkHb-O5yxJoH4Xq3L1WZRvWDHRdg-jCz2WAnc_WNtKJ439flhHEqMKDkrY/s1600/store_account.png" imageanchor="1" style=""><img border="0" height="320" width="279" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSOdYKCKPHuCFt-h712XGoLqILkMA_YvPee2BiaMDNVJ-CS-nGzGFhLHOQilsIsE4JrdLYQ0blBAV9atNymlkHb-O5yxJoH4Xq3L1WZRvWDHRdg-jCz2WAnc_WNtKJ439flhHEqMKDkrY/s320/store_account.png" /></a></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEievdjB_FT4utloUF6CUOBYUZg7qXL9lw77hwdhRN_uMK6KlPL39ZblsL7lQbCCgmQioHqVt09A1XSOfYFojexJs6wfuGEsQ3Ryma1cT-V13WkeecACnC_e9aAxS4mg6-44nJVvLFsi4Lc/s1600/store_subscriptions.png" imageanchor="1" style=""><img border="0" height="320" width="277" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEievdjB_FT4utloUF6CUOBYUZg7qXL9lw77hwdhRN_uMK6KlPL39ZblsL7lQbCCgmQioHqVt09A1XSOfYFojexJs6wfuGEsQ3Ryma1cT-V13WkeecACnC_e9aAxS4mg6-44nJVvLFsi4Lc/s320/store_subscriptions.png" /></a></div>4. 比較多人可能會遇到的問題是我要怎麼知道subscription已經因為使用者取消註冊而過期失效了呢?<br />
方法是App中可以透過http post去詢問subscription狀態,<br />
你需要的是記住最後一次交易的receipt (<code>-[SKPaymentTransaction transactionReceipt]</code>),<br />
或是透過restoreTransaction來取得transaction object,再透過其中的receipt來詢問 。<br />
相關的文件在這 <a href="http://developer.apple.com/library/IOS/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/RenewableSubscriptions/RenewableSubscriptions.html#//apple_ref/doc/uid/TP40008267-CH4-SW4">Verifying an Auto-renewable Subscription Receipt</a><br />
下面是如果成功的話會回傳的結果,status = 0 代表成功。<br />
而last_receipt_info可能會跟receipt不一樣,因為有可能server會自動renew subscription。<br />
<br />
<pre class="code">{
"latest_receipt_info" = {
bid = "com.smartq.MyStore";
bvrs = "1.0";
"expires_date" = 1316424354739;
"expires_date_formatted" = "2011-09-19 09:25:54 Etc/GMT";
"item_id" = 465588051;
"original_purchase_date" = "2011-09-19 08:34:24 Etc/GMT";
"original_transaction_id" = 1000000008159291;
"product_id" = "com.smartq.mystore.sub1m";
"purchase_date" = "2011-09-19 09:20:54 Etc/GMT";
quantity = 1;
"transaction_id" = 1000000008162671;
};
receipt = {
bid = "com.smartq.MyStore";
bvrs = "1.0";
"expires_date" = 1316424354739;
"expires_date_formatted" = "2011-09-19 09:25:54 Etc/GMT";
"item_id" = 465588051;
"original_purchase_date" = "2011-09-19 08:34:24 Etc/GMT";
"original_transaction_id" = 1000000008159291;
"product_id" = "com.smartq.mystore.sub1m";
"purchase_date" = "2011-09-19 09:20:54 Etc/GMT";
quantity = 1;
"transaction_id" = 1000000008162671;
};
status = 0;
</pre>而如果失敗的話可能會如下所示,<br />
status = 21006代表subscription expired。<br />
注意的是這次他不會回傳latest_receipt_info,而是lastest_expired_receipt_info<br />
<br />
<pre class="code">{
"latest_expired_receipt_info" = {
bid = "com.smartq.MyStore";
bvrs = "1.0";
"expires_date" = 1316424354739;
"expires_date_formatted" = "2011-09-19 09:25:54 Etc/GMT";
"item_id" = 465588051;
"original_purchase_date" = "2011-09-19 08:34:24 Etc/GMT";
"original_transaction_id" = 1000000008159291;
"product_id" = "com.smartq.mystore.sub1m";
"purchase_date" = "2011-09-19 09:20:54 Etc/GMT";
quantity = 1;
"transaction_id" = 1000000008162671;
};
receipt = {
bid = "com.smartq.MyStore";
bvrs = "1.0";
"expires_date" = 1316424354739;
"expires_date_formatted" = "2011-09-19 09:25:54 Etc/GMT";
"item_id" = 465588051;
"original_purchase_date" = "2011-09-19 08:34:24 Etc/GMT";
"original_transaction_id" = 1000000008159291;
"product_id" = "com.smartq.mystore.sub1m";
"purchase_date" = "2011-09-19 09:20:54 Etc/GMT";
quantity = 1;
"transaction_id" = 1000000008162671;
};
status = 21006;
}
</pre>5. 在開發中我們都是在sandbox mode中測試,但是subscription的週期會因為為了測試方便會短很多,相關的mapping如下<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghj8Q0O1C-s7ye7JAmnnXg_ACsDq5c8XUimGvhJNFWvpWSCULo6TPs1uq-sVMbs8Yzo0-Ge8jssohTSrJiX6XRvnq54UrwqpgDoy6vn1aiPPyLkXaZchejbANba9kXJTLFGzea45e14Y0/s1600/sandbox.png" imageanchor="1" style=""><img border="0" height="218" width="282" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghj8Q0O1C-s7ye7JAmnnXg_ACsDq5c8XUimGvhJNFWvpWSCULo6TPs1uq-sVMbs8Yzo0-Ge8jssohTSrJiX6XRvnq54UrwqpgDoy6vn1aiPPyLkXaZchejbANba9kXJTLFGzea45e14Y0/s320/sandbox.png" /></a></div>6. 在sandbox mode中只會自動展期五次,第六次會直接expire。<br />
<br />
<b>其他注意事項</b><br />
1. IAP只能在實機上測試,並且只能在sandbox mode下測試,而sandbox mode也只能用測試帳號測試。<br />
2. 測試帳號必須要先在你的itunes connect中,簽署“iOS Paid Applications”這份contract才可以產生。<br />
3. 因為IAP product的價錢可以透過itunes connect上調整,建議商品售價可以透過SKProductRequest去跟itunes connect詢問,而非寫死在code。<br />
下面是4/12/2013新增的<br />
4. Test account請不要在系統的設定(Settings)去登入,也不要留下信用卡帳號,因為一旦使用就不能在sandbox mode中使用<br />
5. 如果你有gmail帳號,你可以用gmail的alias來新增Test account。例如你原本的email是 example@gmail.com,那你可以用example+test@gmail.com去申請。這樣就可以申請一堆測試帳號。<br />
6. 即使是測試帳號,交易過的就不能移除,這對non-consumable product測是很麻煩,唯一的解法就是多產生幾個測試帳號。<br />
<br />
<b>Ref</b><br />
[1] <a href="https://itunesconnect.apple.com/docs/iTunesConnect_DeveloperGuide.pdf">iTunes Connect Developer Guide 7.0</a><br />
[2] <a href="http://developer.apple.com/library/IOS/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction/Introduction.html">In App Purchase Programming Guide </a>popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com6tag:blogger.com,1999:blog-8260844445527494264.post-43679080902433956652011-08-29T22:26:00.008+08:002011-08-30T09:09:27.237+08:00探討Objective-C Block (part 3)前面講了簡單的block用法跟block variable,<br />
但是block難的地方應該就是記憶體管理的部份,<br />
如果你不是很瞭解block內部的記憶體管理,<br />
很容易一個不小心就導致circular reference而導致memory leakage..<br />
<br />
前篇有提到,我們可以把block當作參數傳給function或是message,<br />
但是傳進去後,有可能這個function會想把你的block pointer留下來<br />
最常見的就是做event handling的例子,<br />
我們把一個事件觸發的block當作參數丟進來,<br />
但是是件觸發可能是數秒之後的事情,<br />
而此時註event handler的function/message已經return了,<br />
那此block所reference的local變數可能已經invalid了,<br />
這時候block要怎麼處理這樣的情形呢?<br />
<br />
首先,在block的定義中,此block還停留在call stack之中,<br />
也就是他的生命週期會隨著定義此block的function return之後,其生命週期就會結束<br />
除非我們呼叫<code>block_copy</code>或是<code>[myblock copy]</code><br />
此時block才會從stack變到heap中。<br />
之後我們才可以把參數傳過來的block指給instance variable或是global variable,<br />
而block中所用到的物件在此同時reference count也會+1。<br />
但reference count +1這件事情卻在每一種case不一樣<br />
因為block內部可以使用環境中看的到local, block, instance, local static, global variable<br />
那copy這個動作會發生什麼事情呢?<br />
我們先寫一個範例code<br />
<br />
<pre class="code">//MyBlockTest.h
#import <Foundation/Foundation.h>
typedef void (^myBlockTest_type)(void);
@interface MyBlockTest : NSObject {
NSObject* instanceRef;
myBlockTest_type myBlock;
}
- (void) test;
@end
</pre><br />
<pre class="code">//MyBlockTest.m
#import "MyBlockTest.h"
@implementation MyBlockTest
static NSObject* globalRef;
+(void) initialize
{
globalRef = [NSObject new];
}
- (id)init
{
self = [super init];
if(self)
{
instanceRef = [NSObject new];
}
return self;
}
- (void) test
{
// Local variable
NSObject* localRef = [NSObject new];
// Block variable
__block NSObject* blockRef = [NSObject new];
// Local static variable
static NSObject* localStaticRef;
if(!localStaticRef) localStaticRef = [NSObject new];
// create a block
myBlockTest_type aBlock =
^{
NSLog(@"%@", localRef);
NSLog(@"%@", blockRef);
NSLog(@"%@", instanceRef);
NSLog(@"%@", globalRef);
NSLog(@"%@", localStaticRef);
};
//copy the block
myBlock = [aBlock copy];
NSLog(@"%d", [localRef retainCount]);
NSLog(@"%d", [blockRef retainCount]);
NSLog(@"%d", [instanceRef retainCount]);
NSLog(@"%d", [globalRef retainCount]);
NSLog(@"%d", [localStaticRef retainCount]);
[localRef release];
}
@end
</pre><br />
大家可以先想想看,當呼叫test的時候,會印出什麼樣的結果?<br />
正確的答案是<br />
<code>2 1 1 1 1</code><br />
不知道你答對了沒?<br />
<br />
第一個<code>localRef</code>應該最能夠理解,基本上就是+1,這個就是這樣設計。<br />
<br />
第二個 <code>blockRef</code>,由前面一張對block variable的解釋,<br />
我們可以知道block variable是一個closure用一份。<br />
因此此block variable並沒有額外的retain的動作。<br />
所以被block variable指到的物件也不會有reference count +1的情況。<br />
<br />
第三個<code>instanceRef</code>為什麼沒有+1呢?<br />
事實上這個問題也是挺有陷阱題的味道。<br />
對block來講,他看到的是self這個變數,而非instanceRef。<br />
所以ref. count +1的不是instanceRef而是self。<br />
如果在block copy的前後各把self的ref count印出來你就可以佐證這個事實了。<br />
<br />
第四個<code>globalRef</code>跟第五個<code>localStaticRef</code>本質上很像,所以兩個可以一起討論。<br />
由於這兩個變數在runtime中的位置是固定而且唯一的,<br />
所以基本上在block內用上面兩個變數跟block沒有什麼兩樣。<br />
因此block copy並不會也不需要增加ref. count的數目。<br />
<br />
瞭解之後,那什麼時候可能會出現circular reference呢?<br />
其實跟我們之前聊到的<a href="http://popcornylu.blogspot.com/2011/07/delegate.html">ios delegate你必須知道的事情</a>所說的內容很像。<br />
只是這次主角從delegate換成block。<br />
試想,如果有3個view controller,分別是VC1, VC2, VC3<br />
如果VC1產生並retain VC2<br />
VC2也產生VC3<br />
而且VC2可能跟VC3註冊了一個event handler並且參數是用一個block。<br />
在這個block中可能長這樣。<br />
<pre class="code">[vc3 setOnClose:^{
[self dismissModalViewControllerAnimated:YES];
}];
</pre>那這樣會發生什麼是情呢?<br />
答案是當VC1 release VC2的時候,<br />
VC2因為自己有參照VC3,所以VC3的retain count還是1<br />
VC3因為他的instance variable有retain這個block<br />
而這個block因為用到block中的self<br />
這個self就是VC2,<br />
那這樣可糟了個糕,circular的悲劇就產生了。<br />
<br />
目前官方文件告訴我們要這樣做<br />
<pre class="code">__block VC2* tempVC2 = self;
[vc3 setOnClose:^{
[tempVC2 dismissModalViewControllerAnimated:YES];
}];
</pre>我們透過block variable不會retain的特性,<br />
來把self丟給tempVC2,<br />
如此在block在被copy的時候不會增加retain count。<br />
我只能說太不friendly了,<br />
不過目前好像也只有這樣解,而且到了ARC之後這個問題還是存在。<br />
所以大家一定要改清楚block的memory management,<br />
才不會不知道為什麼,reference count永遠不會歸零的狀況。<br />
<br />
<a href="http://popcornylu.blogspot.com/2011/08/objective-c-block.html">探討Objective-C Block (part 1) - block的使用</a><br />
<a href="http://popcornylu.blogspot.com/2011/08/objective-c-block-part-2.html">探討Objective-C Block (part 2) - block變數</a><br />
探討Objective-C Block (part 3) - block的記憶體管理<br />
popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com1tag:blogger.com,1999:blog-8260844445527494264.post-49250499178371020472011-08-29T22:06:00.002+08:002011-08-30T09:09:55.982+08:00探討Objective-C Block (part 2)原本只打算寫兩篇的,但是忽然覺得很多東西可以講。<br />
而且原本在這篇就要講的有關記憶體的問題,<br />
我選擇了往後擱著<br />
我們來先探討block variable,也就是<code>__block</code>這個修飾字。<br />
<br />
前一篇有提到,block在別的語言叫做closure,<br />
這個closure的概念很像是把環境閉鎖起來,<br />
而這個環境就是指定義該block的這個call stack frame。<br />
當block被呼叫<code>block_copy</code>的時候,或是<code>[someBlock copy]</code>的時候<br />
這個block就會進入heap,<br />
並且會指到目前的stack frame形成closure。<br />
而block variable會跟這個closure綁住,<br />
所以可能說closure variable會更加的貼切。<br />
<br />
我們來看下面一個有趣的例子<br />
<pre class="code">typedef NSUInteger (^countdown_type)(void);
countdown_type createCountdown(NSUInteger number)
{
__block NSUInteger counter = number;
return [[^NSUInteger{
return counter--;
} copy] autorelease];
}
</pre>這個function產生了一個block,這個block中有一個block variable,<br />
起始值是由傳進來的參數決定。<br />
之後每呼叫一次counter都會減1,並且把原本的值傳回去。<br />
下面使用的範例<br />
<pre class="code">countdown = createCountdown(10);
NSLog(@"%d", countdown()); //10
NSLog(@"%d", countdown()); //9
NSLog(@"%d", countdown()); //8
</pre><br />
所以我們更能清楚知道local variable跟block variable的差異。<br />
local variable隨著function回傳而該變數的位置就隨著call stack pop掉。<br />
而如果local static variable跟block variable的比較<br />
static local整個app只有一份。<br />
但是block variable是一個closure一份,<br />
所以以這個例子,如果我們用的是local static,則所有的countdown都共用一個counter。<br />
而如果是用block variable,則每個block都各自有自己的一份。<br />
<br />
如果你比較瞭解了block variable的定義,<br />
你可以在回頭想想為什麼在block中local variable只能當常數使用,<br />
而block variable可以當變數使用。<br />
相信你心中已經有答案了 :D<br />
<br />
<a href="http://popcornylu.blogspot.com/2011/08/objective-c-block.html">探討Objective-C Block (part 1) - block的使用</a><br />
探討Objective-C Block (part 2) - block變數<br />
<a href="http://popcornylu.blogspot.com/2011/08/objective-c-block-part-3.html">探討Objective-C Block (part 3) - block的記憶體管理</a><br />
popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com1tag:blogger.com,1999:blog-8260844445527494264.post-59866474824116085242011-08-24T23:34:00.017+08:002011-08-30T09:10:27.956+08:00探討Objective-C Block (part 1)在ios4推出後,出現了一個新的語言功能block。這個功能其實有接觸多種程式語言的話,會知道其實這個功能不算是Objctive-C特有的<br />
別的語言可能叫做Lambda或是Closure,<br />
這東西主要的特性是他除了是function外,但是更紀錄了此function外部的環境。<br />
也許有點抽象,我先舉個javascript的例子<br />
<pre class="code">var i = 2;
var func = function(){ i = i * i;};
func(); // than i = 4
func(); // than i = 16
</pre>我們在中間定義一個function並且指派到func這個變數,<br />
而在這邊可以用i這個外面的變數,因為在javascript中這樣的定義的就是一個closure,<br />
他會記住外面有i這個變數,<br />
所以之後我們連續呼叫兩次func,<br />
則可以得到i是16這個結果<br />
這個跟objective-c的block,或是別的語言的lambda,closure都是一樣的東西<br />
相關的資料可以參考<a href="http://en.wikipedia.org/wiki/Closure_(computer_science)">Clousre - wikipedia </a> 裡面的介紹。<br />
<br />
回到objective-c的block,當然這個好物當然要好好的利用一下,<br />
這個東西最吸引人的地方就是可以帶不定數量的變數到function當中,因為你只要環境中可以取得的都可以在function中使用<br />
並且因為block(或是其他語言的closure)通常使用上都是anonymous function,直接藏身在你的code當中,因此可以讓程式碼更加精簡。<br />
但是我覺得剛開始用block的時候,有時候會有知其然而不知其所以然的情況。<br />
尤其是Objective-c最令人頭疼的記憶體管理的部份在block中更是複雜,<br />
如果你不知道你的物件怎麼在block中去使用那會是一件危險的事情,<br />
所以這個主題算是經驗分享文,來分享我現在對block的認知,<br />
而當然ios有很多官方的API都支援block,最有名的就是Grand Central Dispatch (GCD)<br />
但是這篇不會介紹GCD,我們把內容專注在block這個語言功能。<br />
<br />
在講block怎麼用之前,容我先介紹C的function跟function pointer,我認為在學block之前要有這個基本的認識比較好。<br />
一來是block跟funtion有點相似,但是又有點不一樣,知道function再來看block我覺得會比較透徹的瞭解<br />
二來它們兩個定義的部份真的很像,但是實作一個function跟block卻大不相同,仔細想想兩個的差異絕對會對block更有深刻的感覺。<br />
首先function相信大家都會實作function,這再簡單不過了<br />
<pre class="code">int plusone(int a)
{
return a+1;
}
</pre><br />
<br />
上面這裡定義了一個傳入參數是int回傳是int的function,這應該不用解釋太多了。<br />
那function pointer呢? 可能開始有些人不是那麼熟悉了...<br />
<pre class="code">int ooxx()
{
typedef int (*myfunc_type)(int a);
myfunc_type myfunc = plusone;
NSLog(@"return value = %d", myfunc(5));
}
</pre><br />
這邊開始就要解釋了<br />
第一行是我定義一個myfunc_type這個function pointer type,而這個function type是傳入是int,回傳是int<br />
而第二行是定義一個myfunc_type的變數,我讓他指到我們剛剛定義的plusone<br />
而第三行我們就是去呼叫這個function pointer,則他就會等同去呼叫plusone,並且帶入5。<br />
如果上面的code你看懂了,恭喜你,block幾乎沒差太多,甚至更好用。<br />
<pre class="code">int ooxx()
{
typedef int (^myblock_type)(int a);
myblock_type myblock = ^int(int a){
return a+1;
};
NSLog(@"return value = %d", myblock(5));
}
</pre><br />
第一行跟function pointer很像,原本function pointer是用*,但是block是用^<br />
第二行也是定義一個myblock_type的變數,但是不一樣的是他指到的是一個block,<br />
block的實作也是^開頭,緊接著是回傳type,接著是傳入的參數,後面接上{....}就是block實作的內容啦。<br />
就像前面所說的,block本身就是anonymous function的方式去定義,<br />
有些時候anonymous function非常好用,例如定義event handler,<br />
我們可以註冊event handler時,直接在後面寫event handler的邏輯,可以很快的可以看到事件發生之後會要做哪些動作<br />
不必像用function pointer或是delegate的方式需要把邏輯寫在另外一個function,<br />
第三行(應該說第三個statement)就跟function pointer很像了,就是呼叫這個block。<br />
<br />
當然block強大的地方就是block所包含的code可以使用外面的變數<br />
例如下面的例子,temp是block外面所定義的,<br />
但是我們可以直接拿來用<br />
<pre class="code">int ooxx()
{
int temp = 5;
typedef int (^myblock_type)(int a);
myblock_type myblock = ^int(int a){
return temp+1;
};
NSLog(@"return value = %d", myblock(5));
}
</pre><br />
但是值得注意的是,上面的code,在block中會把temp當作常數看待,也就是說當定義block的時候,temp的值是5<br />
即便我們在後來把temp改成1<br />
但是得到的結果不會變,<br />
這同樣也說明了一個特性,<br />
那就是我們不能在block中,直接的修改(write)一個外部(區域)變數的值,<br />
所以看下段code<br />
<pre class="code">int ooxx()
{
int temp = 5;
typedef int (^myblock_type)(int a);
myblock_type myblock = ^int(int a){
temp++;
return temp;
};
NSLog(@"return value = %d", myblock(5));
}
</pre><br />
這邊compile就會錯了,因為這邊不只是會讀temp的值,還會寫temp的值。<br />
但是如果我們加上<code>__block</code>這個變數修飾..<br />
那此變數就可以在block中修改。<br />
下面就是個例子<br />
<pre class="code">int ooxx()
{
__block int temp = 5;
typedef int (^myblock_type)(int a);
myblock_type myblock = ^int(int a){
temp++;
return temp;
};
NSLog(@"return value = %d", myblock(5));
}
</pre><br />
以上就是常用的block的方法<br />
事實上更常用的用法會是把block當參數傳到function,<br />
這邊舉個GCD的例子,<br />
<pre class="code">dispatch_async(queue, ^(void) {
// some task here
});
</pre>這邊就是把一個block丟到queue中等待被背景執行,<br />
而就是丟進一個傳入是void傳回是void的block<br />
由於傳回是void,所以可以把<code>^void(void){...}</code>簡化成<code>^(void){...}</code><br />
在block的使用上常常會見到這種把block當參數的寫法,之後相信會漸漸的習慣。<br />
<br />
探討Objective-C Block (part 1) - block的使用<br />
<a href="http://popcornylu.blogspot.com/2011/08/objective-c-block-part-2.html">探討Objective-C Block (part 2) - block變數</a><br />
<a href="http://popcornylu.blogspot.com/2011/08/objective-c-block-part-3.html">探討Objective-C Block (part 3) - block的記憶體管理</a>popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com2tag:blogger.com,1999:blog-8260844445527494264.post-32589776026019501332011-07-22T17:23:00.016+08:002011-08-29T22:53:05.174+08:00ios delegate你必須知道的事情當你開始寫iOS程式不久,應該開始面對到很多的delegate,<br />
不管是用別人的library或是自己寫library,可能都逃不了delegate。<br />
為了怕有些人不知道什麼是delegate,在這邊簡單的介紹一下,<br />
delegate中文叫做委託,通常會用在class內部把一些事件處理"委託"給別人去完成。<br />
舉個例子,XML Parser可能他知道怎麼parse xml,但是parse到的東西要怎麼處理xml parser可能不知道。<br />
所以NSXMLParser就提供了一個NSXMLParserDelegate給client去實作,<br />
當parse到某個element的時候,就callback delegate所定義的message,<br />
讓他client自己去決定怎麼去處理這個element。<br />
好吧,我承認我解釋的很模糊,不過我這篇本來就不是要你搞懂什麼是delegate,<br />
而是針對使用或是設計delegate的時候,可能會要注意的事情。<br />
<br />
在我們的class中設計delegate的時候,我們通常會有幾個注意事項。<br />
假設我的class叫做MyClass,那我們可能會有定義一個MyClassDelegate這個protocol當作我的delegate protocol。<br />
而MyClass中我們可能是這樣寫。<br />
<pre class="code">@protocol MyClassDelegate <NSObject>
- (void) myClassOnSomeEvent:(MyClass*)myClass;
@end
@interface MyClass
{
id<MyClassDelegate> _delegate;
}
@property (nonatomic, assign) delegate;
@end
</pre>上面的code我們注意到<code>delegate</code>此property是定義為<code>@property (assign)</code>。<br />
為什麼我們不用retain而要用assign呢? <br />
原因就是在於iOS的reference counting的環境中,我們必須解決circular count的問題。<br />
讓我們來寫寫我們平常都怎麼用delegate的,下面的code我想大家應該不陌生<br />
<pre class="code">- (void)someAction
{
myClass = [MyClass new];
myClass.delegate = self;
....
}
</pre>這邊很快的就出現circular reference了<br />
假設上面的code是寫在一個myViewController的物件當中,<br />
之後一旦myViewController的reference count變成1的時候,<br />
myViewController跟myClass這兩個兄弟兩只剩下互相retain,那就變成了孤島,也就因此造成了memory leak!!!<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi50YSMc8RLOTnHgCRxJeFx4UxYef0a4hl0giII2Zw2bdkGw81lmk0eLpIn0aNkDJgRBPu754nKi3Ed0X3ABeSy0-goJWTk0jEgF6OV-KaCIrUWpX0BapCIJGusGGrDoexOP2IKj0IdLDM/s1600/delegate1.png" onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}"><img style="cursor:pointer; cursor:hand;width: 320px; height: 137px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi50YSMc8RLOTnHgCRxJeFx4UxYef0a4hl0giII2Zw2bdkGw81lmk0eLpIn0aNkDJgRBPu754nKi3Ed0X3ABeSy0-goJWTk0jEgF6OV-KaCIrUWpX0BapCIJGusGGrDoexOP2IKj0IdLDM/s320/delegate1.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5632104295644286354" /></a><br />
<br />
也因為這樣,iOS官方文件才會要建議我們所以的delegate都要用assign property。<br />
也就是所謂"weak reference"的property,他的特色就是雖然會持有對方的reference,但是不會增加retain count。<br />
如此下來,當myViewController的retain count變成0,則會dealloc。<br />
同時在dealloc中,也一併把myClass release,則myClass也跟著被release。<br />
<pre class="code">- (void)dealloc
{
[myClass release];
[super dealloc];
}
</pre><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF8r5YK-CC6QsHYRgECagmztqsZ8v-OGNIvMIpul9RdNZPl5DOkb4iUxptXfg5vW1B377rL8laQr26h72v0rzSxX0bR6JPa5r856nme9S8Rad-TXLH6CadSR1LCSsh9xSTKjGTbh726CM/s1600/delegate2.png" onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}"><img style="cursor:pointer; cursor:hand;width: 320px; height: 186px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF8r5YK-CC6QsHYRgECagmztqsZ8v-OGNIvMIpul9RdNZPl5DOkb4iUxptXfg5vW1B377rL8laQr26h72v0rzSxX0bR6JPa5r856nme9S8Rad-TXLH6CadSR1LCSsh9xSTKjGTbh726CM/s320/delegate2.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5632104294537984130" /></a><br />
<br />
事情就結束了嗎? 還沒有唷...<br />
這邊還有一個大家常常忘記的重點,那就是上面的dealloc這樣寫會有潛在危險。<br />
應該要改成這樣<br />
<pre class="code">- (void)dealloc
{
<font color='red'>myClass.delegate = nil;</font>
[myClass release];
[super dealloc];
}
</pre>你可能會很納悶,myClass不是馬上就會被release了嗎? 幹嘛要先把他的delegate設成nil?<br />
那是因為我們假設myClass會馬上會被dealloc,但是現實狀況這個是不一定的,<br />
有可能裡面內部有建個NSURLConnection,或是正在做某件事情而讓其他物件也retain myClass。<br />
如果myClass沒有馬上dealloc,那他的<code>myClass.delegate</code>不就正指向一個不合法的位置了嗎? (此種pointer稱作dangling pointer)<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgwB_YJ5jqrlK4YPu_Poh3fZvBq5YZ3POg5kNL-44Qa_9XwE9TaOQxniCncBPSWHocChRpuBo-vpksPD0BMuz5ihBxaGtRllvBurOUXfWDGm-uEWTSPB-AApJCX1jOemZWfNST5fpmEyio/s1600/delegate3.png" onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}"><img style="cursor:pointer; cursor:hand;width: 320px; height: 184px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgwB_YJ5jqrlK4YPu_Poh3fZvBq5YZ3POg5kNL-44Qa_9XwE9TaOQxniCncBPSWHocChRpuBo-vpksPD0BMuz5ihBxaGtRllvBurOUXfWDGm-uEWTSPB-AApJCX1jOemZWfNST5fpmEyio/s320/delegate3.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5632104299513124754" /></a><br />
<br />
解決方法是在MyViewController的dealloc中,在release myClass之前,<br />
要先把原本指向自己的delegate改設成nil,這樣才可以避免crash發生。<br />
在我之前寫的project,很大一部份的crash都是這樣造成的,因為這個問題通常不是每次都發生,<br />
但是發生的時候確很難在重新複製,所以不可不慎啊。<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEig51BvsKaCYvFh2kiBqJFy2eGD98-Sei7xxhs-x6XaJ9dn1EUWRI6HsZYnfN8lThBR0AYYew4clI2H_Lc9Lr0w3V_43O-dXCcLdX39C3weRTCZO-F8beF7Ia3xhSmmWiWNGZzZZC6dbqs/s1600/delegate4.png" onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}"><img style="cursor:pointer; cursor:hand;width: 320px; height: 182px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEig51BvsKaCYvFh2kiBqJFy2eGD98-Sei7xxhs-x6XaJ9dn1EUWRI6HsZYnfN8lThBR0AYYew4clI2H_Lc9Lr0w3V_43O-dXCcLdX39C3weRTCZO-F8beF7Ia3xhSmmWiWNGZzZZC6dbqs/s320/delegate4.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5632104306871986450" /></a><br />
<br />
但是很興奮的是到了iOS5中的<a href="http://popcornylu.blogspot.com/2011/06/arc-automatic-reference-counting.html">Automatic Reference Counting</a>這個問題可以有所改善。<br />
在ARC中提出了一個新的weak reference的概念來取代原本的assign,<br />
weak reference指到的物件若是已經因retain count歸零而dealloc了,則此weak reference也自動設成nil。<br />
而原本舊的這種assign的作法,在ARC中叫做__unsafe_unretained,這只是為了相容iOS4以下的版本。<br />
<br />
回顧重點:<br />
如果你是寫library給別人用的,記得把你的delegate設成assign property,這樣才不會造成circular reference<br />
當你是要始用別人的library,記得在你自己dealloc的時候,把delegate設成nil,以避免crash的事情發生。<br />
<br />
<span style="font-weight:bold;">References</span><br />
[1] <a href="http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CocoaFundamentals/CommunicatingWithObjects/CommunicateWithObjects.html#//apple_ref/doc/uid/TP40002974-CH7-SW16">Ownership of Delegates, Observers, and Targets</a><br />
popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com2tag:blogger.com,1999:blog-8260844445527494264.post-86032700816954924002011-07-12T23:04:00.011+08:002011-07-12T23:28:57.118+08:00Flurry - Mobile Application Analytics<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1GjMi3jl0r4z2HHdBrWYtMVMsjXWOIVjFBzPUiqnj3HHXLWv2G4WGtD1TWZa7TVJJ1kKTTQxsFtHcb3oqT1zBYY_4bqrKMkIEyI-GFVeHNEWAGLWUZWFgwtHG0d6hQ3M5jgOGKJtTGvs/s1600/logo.png"><img style="cursor:pointer; cursor:hand;width: 145px; height: 42px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1GjMi3jl0r4z2HHdBrWYtMVMsjXWOIVjFBzPUiqnj3HHXLWv2G4WGtD1TWZa7TVJJ1kKTTQxsFtHcb3oqT1zBYY_4bqrKMkIEyI-GFVeHNEWAGLWUZWFgwtHG0d6hQ3M5jgOGKJtTGvs/s320/logo.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5628482508344263058" /></a><br />原本想說要一個禮拜寫一篇文章的<br />想不到才一開始就懶散了<br />趁今天晚上下雨沒有辦法踢足球的晚上<br />再來趕功課<br /><br />今天聽到客戶說要分析我們的ipad app上使用者的行為<br />希望可以知道使用者點了哪些item 還有做了哪些事情<br />希望進一步份析使用者的使用狀況<br />相信有寫web的應該二話不說就直接想到Google Analytics<br />但是到了mobile領域 還真不知道使用什麼東西勒<br />經過學長的指點 說有一個service叫做<a href="http://www.flurry.com/">Flurry</a>好像就是在做這樣的服務<br />用了一下還真的挺簡單易用了<br />在此就跟大家報告一下<br /><br />要分析使用者的行為我的需求如下<br />1. 可以自訂我想監控的行為<br />2. 很簡單的可以插入我的code<br />3. 最好可以在離線的時候也可以統計使用者行為<br />4. server端有類似Google Analytics的報表介面<br />5. 有API可以抓報表資料整合外部的系統<br /><br />想不到Flurry是五個願望一次滿足,<br />而且我只用了一個小時的時間就知道怎麼用了<br />完全沒有花到太多的功夫。<br />以下是使用方法及功能介紹<br /><br /><span style="font-weight:bold;">1. 註冊帳號</span><br />首先連到它們的官方網站 <a href="http://www.flurry.com/">http://www.flurry.com/</a> <br />註冊一個帳號 並且新增加一個application 並選擇你的平台 (我當然是選iPad, iPhone, iPod..)<br />之後就可以根據你選擇的平台Download SDK,而它也會給你這個app的application key<br />我們之後就必須用這個key來跟Flurry互動<br /><br /><span style="font-weight:bold;">2. 整合進你的project</span><br />a. 解開sdk,裡面主要有兩個檔案需要加到我們的project<br /><code>FlurryLib/FlurryAPI.h</code><br /><code>FlurryLib/libFlurry.a</code><br /><br />b. 在你的AppDelegate的進入點,塞進初始化function<br /><pre class="code"><br />#import "FlurryAPI.h"<br /><br />- (void)applicationDidFinishLaunching:(UIApplication *)application {<br /> [FlurryAPI startSession:@"YOUR_API_KEY"];<br />}<br /></pre><br /><br /><br /><span style="font-weight:bold;">3. 追蹤使用者行為</span><br />如果你想監控使用者是否按下一個button,你可以在對應的action加上下面這段code<br /><code>[FlurryAPI logEvent:@"EVENT_NAME"];</code><br />這個event name可以自己定義,每個event name會變成server端的一個獨立數據<br /><br /><br />如果想用類似網站的pageview概念<br />可以使用<br /><code>+[FlurryAPI logAllPageViews:]</code><br /><code>+[FlurryAPI logPageView]</code><br />前者可以帶一個參數是UINavigationController或是UITabBarController,在每次切換的動作發生時,自動增加一次page view。<br />後者是手動的增加一次page view<br /><br /><span style="font-weight:bold;">4. 上傳追蹤資料。</span><br />上傳的動作是完全自動的。<br />Flurry會在app開啓或是關閉的時候自動的把這些追蹤資料整包的丟給server,<br />我們完全不用寫任何一段code。<br />另外因為他的做法本來就是先把這些使用者的行為放在local,<br />所以即使離線也可以紀錄,<br />直到下次使用者打開或是關閉app的時後是有連上網的時候在一併上傳<br /><br /><span style="font-weight:bold;">5. 報表系統</span><br />server端有完整的圖形化介面來統計數據<br />有Dashboard的整體分析<br />還有Usage, Audience, Events, Technical的細項分析<br />基本上功能還算完善<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiKNaNSEsiJnAHMCWB0vOQE1ts332P7vvv3v_RARdCkCBtmgbsbfySph9wtO4sdxL8h-Iff1TTZUIrAC-D2oBNLk0GW4pVvQCMKY1VZ__VP-pPfRMKS50TqejU5bmEGok0p_JBdrEuopMY/s1600/dashboard.png"><img style="cursor:pointer; cursor:hand;width: 320px; height: 249px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiKNaNSEsiJnAHMCWB0vOQE1ts332P7vvv3v_RARdCkCBtmgbsbfySph9wtO4sdxL8h-Iff1TTZUIrAC-D2oBNLk0GW4pVvQCMKY1VZ__VP-pPfRMKS50TqejU5bmEGok0p_JBdrEuopMY/s320/dashboard.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5628482642202776034" /></a><br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXcGvb6J4_rAZJJTb5nH4xhy5W7PxKhMkWMhDnKiVv0zXFgj8rSOiTlpw25vnaPtBt8hUpyhbYHahUniFNjTkdg9kphC11sINimKrkfC1cSTEJC9prwXmYczBmvMRKMXAKTDbG2azD8b4/s1600/usage.png"><img style="cursor:pointer; cursor:hand;width: 320px; height: 250px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXcGvb6J4_rAZJJTb5nH4xhy5W7PxKhMkWMhDnKiVv0zXFgj8rSOiTlpw25vnaPtBt8hUpyhbYHahUniFNjTkdg9kphC11sINimKrkfC1cSTEJC9prwXmYczBmvMRKMXAKTDbG2azD8b4/s320/usage.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5628482911870779986" /></a><br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipYTshaJz-QceG2g4Jm0l5f1h3R1jCekm7nBlRLB5AKjbckupyiKhpkMa3qYSzRqoe_Ew0ohHSW_SYEc_e20djhkgFzJbggpGRnl1POhbCPq6Buh1VxbJa-PAPZYPEaisTMpD6Fj5WNPs/s1600/events.png"><img style="cursor:pointer; cursor:hand;width: 320px; height: 245px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipYTshaJz-QceG2g4Jm0l5f1h3R1jCekm7nBlRLB5AKjbckupyiKhpkMa3qYSzRqoe_Ew0ohHSW_SYEc_e20djhkgFzJbggpGRnl1POhbCPq6Buh1VxbJa-PAPZYPEaisTMpD6Fj5WNPs/s320/events.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5628483005435365682" /></a><br /><br /><span style="font-weight:bold;">6. 數據資料下載</span><br />如果你有自己的server,想要把這些統計資料整合到自己的系統的話<br />我們也可以用它們的REST API去抓取這些資料<br />如果有需要的話可以連到這邊去看詳細資料<br /><a href="http://wiki.flurry.com/index.php?title=API_Usage">http://wiki.flurry.com/index.php?title=API_Usage</a><br /><br /><br />另外提醒一點的是,<br />在我初次測試的時候,我會想說log的event應該要馬上出現在server<br />事實上他並沒有那麼快的反應,可能會要等個半個小時左右才會更新一次報表 (詳細間隔還需要確認)<br />所以如果你的log沒有出現可能要請你等一會唷..popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com4tag:blogger.com,1999:blog-8260844445527494264.post-26534324328548334752011-06-25T23:02:00.010+08:002011-06-27T10:32:33.353+08:00初探ARC - Automatic Reference CountingWWDC 2011的影片開始下載<br />今天颱風天<br />那就抓個大家之前有討論的ARC來研究一下<br />下面是我的小小心得 :D<br /><br />我想寫過iOS的人<br />絕對最痛很的就是令人髮指的Memory Management<br />就連TBBT的leonard也為此煩惱 <br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSF0uRBFX1C3LCGKi7SalrDBTr7vYuMC7SAfL5I3pIRIHOXBHlYUxRDyw6mjTXOnUhWEBavibXBoi644_UZ7XCEGbaLFqjBT6Gs0FU4Mclwd2ExwESJ2Eiyh3NcC7dRaJwwIGwaKlZCqk/s1600/leonard_mrc.png"><img style="cursor:pointer; cursor:hand;width: 320px; height: 251px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSF0uRBFX1C3LCGKi7SalrDBTr7vYuMC7SAfL5I3pIRIHOXBHlYUxRDyw6mjTXOnUhWEBavibXBoi644_UZ7XCEGbaLFqjBT6Gs0FU4Mclwd2ExwESJ2Eiyh3NcC7dRaJwwIGwaKlZCqk/s320/leonard_mrc.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5622173221838193474" /></a><br />source: <br /><a href="http://digdog.tumblr.com/post/3628378498/actually-sheldon-has-better-design-if-youre">http://digdog.tumblr.com/post/3628378498/actually-sheldon-has-better-design-if-youre</a><br /><br />程式中充斥著retain, release, autorelease<br />相信即使是老手,也難免會恍神而不小心少release而產生memory leakage<br />或是多release而產生bad access直接當掉<br />在這個大家都用garbage collection的時代<br />這種memory management的方式實在讓人寫得很痛苦<br /><br />當然目前objective-c的做法也不能說完全沒有好處<br />畢竟gc的動作是無法預測什麼時候會去執行的<br />也許剛好在你正在忙的時候給你gc一下就卡了<br />這也是為什麼iOS跑起來可以比別家順的其中一個原因。<br />而在iOS5首次提出了ARC(Automatic Reference Counting)這個解決方案<br />試圖讓開發者減少寫Reference Counting的時候產生的錯誤。<br /><br />ARC有哪些重點?<br />1. 它是Compile-time的技術,並不是runtime有一個thread去幫你處理reference count..<br />2. 開發者不能再直接使用retain, release, autorelease,改由compiler幫你塞這些code<br />3. 所有的property不再使用retain, assign而改用strong, weak取代<br />4. NSAutoreleasePool也要改用特有的 <code>@autoreleasepool{ }</code> 來取代<br />沒了!! <br /><br />好啦,其實當然不只這樣,但是先從簡單的來<br />先看下面的code<br /><pre class="code"><br />-(NSString*)helloString<br />{<br /> return [[NSString alloc] initWithFormat:@"Hello %@", _name];<br />}<br /></pre><br />以現在的MRC(Manual Reference Counting)環境<br />這種寫法有潛在的memory leak的風險<br />因為根據命名規則,非new, copy, mutableCopy開頭的message回傳的物件不能是retained object<br />而是要retained and autoreleased object<br />所以可能要改成這樣的寫法<br /><pre class="code"><br />-(NSString*)helloString<br />{<br /> return [NSString stringWithFormat:@"Hello %@", _name];<br />}<br /></pre><br />但是如果使用了ARC之後<br />前後兩個方法都可以<br />主要是因為compiler會分析你的程式碼中呼叫的message是否有含alloc, new, copy, mutableCopy這些開頭<br />如果有的話就知道回傳的object的ownership是你的<br />接著根據你所在的message是否有alloc, new,... 來決定你回傳的物件是哪種類型。<br />以上面message為例子,回傳的應該是一個retained and autoreleased object<br />以往MRC中,我們就要跟leonard一樣,好好的計算一下才不會錯<br />但是在ARC這些複雜的加減法都交給compiler來幫我們搞定,完全不需要知道實作細節。<br /><br />另外memory mangagement還有一個重點是在member variable<br />以前我們定義在class中的member variable並沒有留下是否要retain物件的資訊<br />除非我們透過@property (retain)或是@property (assign)的方法才可以知道個別的ownership語意。<br />但是在ARC中,因為所有retain/release都是要自動的<br />因此所有的Member variable就定義為預設都是strong type,這有點類似@property (retain)的味道<br />除非我們在member variable前加上__weak這個描述才會是weak type,這就有點類似@property (assign)<br />所以在程式碼中,若物件被指到strong pointer,就會自動retain<br />weak pointer則不會retain..<br /><br />另外weak reference還有一個非常吸引我的功能<br />就是所謂的<span style="font-weight:bold;">zeroing weak reference</span>.<br />這個不得不歡呼一下<br />以前我們在用delegate pattern的時候<br />文件都教我們使用@property (assign)來hold delegate object<br />目的就是為了避免circular reference所造成的memory leakage<br />但是常常會遇到delegate所指到的object已經因為retain count歸零而被清掉了<br />若我們還去使用它 系統馬上就當給你看<br />所以以前我們都必須要記得把delegate設成nil<br />在ARC當中,如果weak pointer所指到的object的retain count已經是0的時候<br />則這個weak pointer也會被設成0<br />這就是所謂的"zeroing weak reference"<br />所以在weak pointer中又比以前的@property (assign)更為安全<br /><br />從我最新抓的xcode4.2 with iOS5的環境來看<br />預設已經是把ARC變成預設了<br />而舊的project可以透過refactor的方式Convert to Objective-C ARC<br />究竟麻不麻煩,這我還不敢發表評論 XD<br />但是我相信長痛不如短痛<br />ARC絕對是對objective-c的記憶體管理有非常大的幫助。<br />讓我們準備擁抱ios5的ARC時代吧<br /><br /><span style="font-weight:bold;">References</span><br />[1] <a href="http://clang.llvm.org/docs/AutomaticReferenceCounting.html">http://clang.llvm.org/docs/AutomaticReferenceCounting.html</a>popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com0tag:blogger.com,1999:blog-8260844445527494264.post-20447902480687072372011-06-23T22:33:00.012+08:002011-06-23T23:07:25.568+08:00在ios使用sqlite的救贖 - FMDB最近有個專案需要小型資料庫,當然首選一定是iOS內建的sqlite,但是因為他是C的lib,使用上是出了名的困難。之前就有聽說用sqlite可以使用<a href="https://github.com/ccgus/fmdb">FMDB</a>這個objective-c wrapper library。果不其然的簡單易用,我馬上把我使用經驗跟大家分享,如果你想馬上體驗,也可以去抓我的<a href="https://github.com/popcornylu/MySqlite">Sample project - MySqlite</a>。<br /><br /><b>安裝FMDB</b><br />方法很簡單,就跟所有的open source library一樣的三步驟。<br />1. 首先你要先把 libsqlite3.dylib 加進library <br />2. 在需要用到FMDB的檔案中<code>#import "FMDatabase.h"</code><br />3. 把FMDB的.h跟.m加進你的project中<br />接著就可以開心的使用FMDB<br /><br /><b>產生Database</b><br />基本上就是透過<code>+[FMDatabase databaseWithPath]</code>取產生Database instance。<br /><pre class="code"><br />NSURL *appUrl = [[[NSFileManager defaultManager] <br /> URLsForDirectory:NSDocumentDirectory <br /> inDomains:NSUserDomainMask] lastObject];<br />NSString *dbPath = [[appUrl path] stringByAppendingPathComponent:@"MyDatabase.db"]; <br />FMDatabase* db = [FMDatabase databaseWithPath:dbPath];<br /><br />if (![db open]) { <br /> NSLog(@"Could not open db"); <br />}<br /></pre><br /><br /><b>產生所需table</b><br />其實寫過sql的應該對接下來的動作都不陌生,都一樣就是sql語法囉,詳細的請參考<a href="http://www.sqlite.org/lang.html">sqlite文件</a><br />下面的動作是產生一個user的table,並且有uid, name, descritpion三個column。我們使用<code>IF NOT EXISTS</code>來保證table不存在才會產生table。<br /><pre class="code"><br />if(![db executeUpdate:@"CREATE TABLE IF NOT EXISTS user (uid integer primary key asc autoincrement, name text, description text)"])<br />{<br /> NSLog(@"Could not create table: %@", [db lastErrorMessage]);<br />}<br /></pre> <br /><br /><b>新增record</b><br />在FMDB中,它有提供很貼心類似<code>-[NSString stringWithFormat:...]</code>的功能,但是比較不一樣的是如果你是字串,它會幫你加上括號,使用上更為便利。<br /><pre class="code"><br />if(![db executeUpdate:@"INSERT INTO user (name, description) VALUES (?,?)", name, description])<br />{<br /> NSLog(@"Could not insert data: %@", [db lastErrorMessage]);<br />}<br /></pre><br /><br /><b>刪除record</b><br />一樣是下sql語法,這邊比較需要注意的是如果你要傳進去的是int, double這種基本形態,記得要轉換成物件,也就是用<code>+[NSNumber numberWithInt]</code>去轉換<br /><pre class="code"><br />if(![_db executeUpdate:@"DELETE FROM user WHERE uid = ?", [NSNumber numberWithInt:uid]])<br />{<br /> NSLog(@"Could not delete data: %@", [db lastErrorMessage]);<br />}<br /></pre><br /><br /><b>查詢資料</b><br />在FMDB中有提供另外一個class <code>FMResultSet</code>來存放查詢出來的records,透過<code>-[FMResultSet next]</code>來iterate出所有的record<br /><pre class="code"><br />NSMutableArray* items = [NSMutableArray arrayWithCapacity:0];<br />FMResultSet *rs = [db executeQuery:@"SELECT uid, name, description from user"];<br /><br />while ([rs next]) { <br /> int uid = [rs intForColumn:@"uid"];<br /> NSString *name = [rs stringForColumn:@"name"]; <br /> NSString *description = [rs stringForColumn:@"description"]; <br /> <br /> [items addObject:[NSDictionary dictionaryWithObjectsAndKeys:<br /> [NSNumber numberWithInt:uid], @"uid",<br /> name, @"name",<br /> description, @"description",<br /> nil]];<br />}<br /><br />[rs close]; <br /></pre><br /><br />基本上在其他oo平台中有用過sql的,對於FMDB的設計應該並不陌生,可以說完全不會有太多的困難。而相較於用傳統的plist或是archive的方法,我相信不管是效能上或是功能上,Sqlite+FMDB的組合絕對會更有優勢。對於中型以上的iOS程式應該會是標準配備。popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com10tag:blogger.com,1999:blog-8260844445527494264.post-83966034816501316052011-05-31T12:38:00.006+08:002011-06-23T22:50:53.824+08:00XCode tip: 觀看UI Hierarchy的方法在xcode的console下,我們除了可以看log<br />其實因為它是gdb環境,所以我們還可以對目前的環境做操作<br />我比較常用的是<br /><div class="code"><span style="color:blue">gdb></span> po self</div>來列印中斷點所在的物件<div><br /></div><div>也可以列印所在物件的message所回傳的object<br /><div class="code"><span style="color:blue">gdb></span> po [self view]</div><br />如果該物件是個UIView.. 則可以列印它的UI Hierarchy<br /><div><div class="code"><span style="color:blue">gdb></span> po [[self view] recursiveDescription]</div><div><br /></div>這個是大絕招<br /><div class="code"><span style="color:blue">gdb></span> po [[[[UIApplication sharedApplication] windows] objectAtIndex:0] recursiveDescription]</div><br />這樣就可以隨時pause.. 隨時看UI Hierarchy<br />就不需要在該UIView或UIViewController設定中斷點<br />參考資源<br /><a href="http://goo.gl/alEjL">http://goo.gl/alEjL</a> <br /></div></div>popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com0tag:blogger.com,1999:blog-8260844445527494264.post-2533215409245450702011-03-19T21:41:00.005+08:002011-03-19T21:50:55.584+08:00XCode4一些使用經驗分享XCode4 3/10正式開放下載,小弟馬上抓下來玩玩看。雖然從XCode3轉到XCode4一定會有些不適應,不過很快的也被很多讓開發更加便利的小細節所吸引。這篇是跟大家分享一些小技巧,希望對大家有幫助。<br /><br /><span style="font-weight:bold;">Command-Click / Option-Click</span><br />現在Command按著移到Symbol上面點擊,就可以直接跳到定義的部份<br />還有Option按著移到Symbol上面點擊,就可以直接看這個Symbol的文件<br />(如果有開發iOS的,記得到Preference -> Documentation裡面下載iOS library文件)<br /><br /><span style="font-weight:bold;">Assistant Editor:</span><br />這是 XCode4才有的新功能,方便我們同時修改兩個檔案甚至多個檔案。這對diff兩個檔案,或是同時想修改.h/.m都很有幫助。使用方法很簡單,XCode4右上角的Editor那邊就可以切換了。<br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjp453xhn4HKoX9ZfmdcR1WjRkIctZavfT8DKRG8ykjwx83d2i3myWRZjrtfkXIzHNlkawbTutG2JhKogqbQ-04s-6N5whGPURZRSViuJsX76KLbr8Fr78ieHnWkxfnOOf3JVTcNmTf77Y/s1600/assistant.png"><img style="display:left; margin:0 10px 10px 0;cursor:pointer; cursor:hand;width: 106px; height: 56px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjp453xhn4HKoX9ZfmdcR1WjRkIctZavfT8DKRG8ykjwx83d2i3myWRZjrtfkXIzHNlkawbTutG2JhKogqbQ-04s-6N5whGPURZRSViuJsX76KLbr8Fr78ieHnWkxfnOOf3JVTcNmTf77Y/s320/assistant.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5585786215424956498" /></a><br />而Assistant Editor也有一些進階用法。<br />1. 在左邊瀏覽頁面上,按Option + Click檔案,可以開啓檔案到Assistant editor<br />2. 在Editor中,按Command + Option + Click一個Symbol,可以在Assistant editor開啓。可以跟上面講的Command+Click選擇使用。<br />3. Command + Enter則可以關掉Assistant editor<br /><br /><span style="font-weight:bold;">Tab bar</span><br />不知道這是不是Xcode4才有的功能? 但這個功能在大部份的Editor都有,也很實用。<br />為了方便使用,我修改了一些設定<br />1. View -> Show Tab bar 把他點出來,讓Tab bar可以總是出現<br />2. Preference裡面 General -> Double Click Navigation 改成Use Separate Tab<br />這樣好處就是我在Project Navigators裡面雙擊一個檔案就可以開在一個新的tab<br />同樣的按著command.. 雙擊某個Symbol也可以在新的tab開啓。<br /><br /><span style="font-weight:bold;">Display Navigation Chooser</span><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhVlDGq4miN6TY3a-xy4r5GzYPYMLeEZEsJSHuLMktE-trgHhdTe_OcbpwGg50GV8Xv4IGrheQ75Vfzb-YS6JLP2SzEuMkad-mqLLKNzxurNFwzeNPbDThxVO9OG8arJqgTiS9vks4gQPY/s1600/navigation_chooser.png"><img style="margin:0 0 10px 10px;cursor:pointer; cursor:hand;width: 320px; height: 151px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhVlDGq4miN6TY3a-xy4r5GzYPYMLeEZEsJSHuLMktE-trgHhdTe_OcbpwGg50GV8Xv4IGrheQ75Vfzb-YS6JLP2SzEuMkad-mqLLKNzxurNFwzeNPbDThxVO9OG8arJqgTiS9vks4gQPY/s320/navigation_chooser.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5585786953049559282" /></a><br />如果需要更有彈性的開啓方法<br />可以使用Navigation Chooser<br />1. 在Project Navigator當中用Shift+Option點檔案<br />2. 在Editor中按著Command+Shift+Option去點檔案<br /><br /><br /><span style="font-weight:bold;">整理上面的幾個熱鍵</span><br />在Editor中<br />Command + Click: 同個Editor開Symbol<br />Option + Click: 看Symbol的Document<br />Command + Option + Click: 開在Assistant Editor<br />Command + Shift + Option + Click: 用Navigation Chooser來開<br />Command + DoubleClick: 用新Tab開<br />若在project navigator中<br />Click: 同個Editor開Symbol<br />Option + Click: 開在Assistant Editor<br />Shift + Option + Click: 用Navigation Chooser來開<br />DoubleClick: 用新Tab開<br /><br />另外我覺得這個縮排熱鍵也很實用<br />先框選要排版的範圍<br />Command+[ 集體反縮排<br />Command+] 集體縮排<br />Control + | 集體重新排版<br />切換.h/.m改成<br />Command+Control+Up<br />Command+Control+Down<br />回到前一個位置<br />Command+Control+Left<br />回到下一個位置<br />Command+Control+Right<br /><br />其他還有蠻多新的功能也很不錯 如<br />Auto build: 這點跟Eclipse很像,XCode會隨時在背景build,讓我們在Edit的時候就可以馬上發現是否有錯誤產生<br /><br />Debug: 可以在Debug的時候,滑鼠移到變數上可以直接看變數的內容,甚至可以直接dump description到console<br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5do2TUIarHD7xeFiT__NXyw7D-q4a2Jvkcb4zWpYcN8sw-JJlVnp7A44WwjJUpneSa1KNLMYJArFUCGEF4MGpHCA7sIeZth8Bq8llCAczELz0Jc6x_002A_63WtyCODQuMOKehO8wdcE/s1600/debug.png"><img style="cursor:pointer; cursor:hand;width: 320px; height: 46px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5do2TUIarHD7xeFiT__NXyw7D-q4a2Jvkcb4zWpYcN8sw-JJlVnp7A44WwjJUpneSa1KNLMYJArFUCGEF4MGpHCA7sIeZth8Bq8llCAczELz0Jc6x_002A_63WtyCODQuMOKehO8wdcE/s320/debug.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5585787435763761250" /></a><br /><br />Workspace Window的設計:算是把以前多個Window整合到一個window的設計,這個好壞見仁見智,我是覺得還不錯的。<br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg34zgctWvsFVzWchjd7ywf6AXu8Tu7mGKtHjuRmEdQUGNJZKOcMYCdWiUaoMyRkcMn51R2el_RYZiQkA3Ck3jtSGnai2WYPjp3Bn2-TwqBd32ROaFL5ebqy5pjp26C0aA47TZ5nIQNWrw/s1600/Workspace_window.png"><img style="cursor:pointer; cursor:hand;width: 320px; height: 190px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg34zgctWvsFVzWchjd7ywf6AXu8Tu7mGKtHjuRmEdQUGNJZKOcMYCdWiUaoMyRkcMn51R2el_RYZiQkA3Ck3jtSGnai2WYPjp3Bn2-TwqBd32ROaFL5ebqy5pjp26C0aA47TZ5nIQNWrw/s320/Workspace_window.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5585787585401488194" /></a><br /><br />IB整合到XCode裡面: 這個早就該這樣設計了<br /><br />新的Scheme的概念: 把以前的Build Configuration, Target, Executable都包進Scheme裡面來,這個使用上還需要習慣。popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com1tag:blogger.com,1999:blog-8260844445527494264.post-50006805664041647302010-11-14T15:30:00.007+08:002011-03-19T21:41:09.498+08:00又寫了一個新的jquery plugin - jquery imgdesc最近一個project是需要有產品瀏覽..<br />而每個產品還有詳細描述<br />為了增加畫面的簡潔度跟流暢感<br />所以又寫了一個jquery的plugin imgdesc<br /><a href="http://popcorny.byethost10.com/jquery-imgdesc/">呈現的結果是這樣</a><br /><br />要使用請按照下面的步驟<br />1. 首先先確定你的html的DOCTYPE如下<br /><span style="color:grey"><br /><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><br /></span><br />這樣跑出來的結果位置才會對<br /><br />2. include需要的javascript以及css<br /><pre class="brush:html"><br /><script type="text/javascript" src="js/jquery-1.4.3.min.js"></script><br /><script type="text/javascript" src="js/jquery-imgdesc.js"></script><br /><link type="text/css" rel="stylesheet" href="css/jquery-imgdesc.css"><br /></pre><br /><br />3. 資料的部分,請這樣定義<br /><pre class="brush:html"><br /><div class="modelImage"><br /> <img src="http://www.blogger.com/images/1.jpg" /><br /> <div>Title</div><br /> <div>Your description</div><br /></div><br /><div class="modelImage"><br /> <img src="http://www.blogger.com/images/2.jpg" /><br /> <div>Title</div><br /> <div>Your description</div><br /></div><br /><div class="modelImage"><br /> <img src="http://www.blogger.com/images/3.jpg" /><br /> <div>Title</div><br /> <div>Your description</div><br /></div><br /></pre><br /><br />4. 最後一步就是呼叫imgdesc()套上imgdesc的plugin<br /><pre class="brush:javascript"><br />$(function()<br />{ <br /> $(".modelImage").imgdesc();<br />}); <br /></pre>popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com0tag:blogger.com,1999:blog-8260844445527494264.post-48584419427144492252010-10-26T15:54:00.002+08:002011-03-19T21:40:47.868+08:00用jquery來實作多層menu最近開始玩jquery<br />拿了一個手中案子的多層Menu當作練習..<br />並且把這個menu做成jquery的plugin<br />出來的結果是<a href="http://popcorny.byethost10.com/jquery-menu/">這樣</a><br /><br />使用方法很簡單...<br />首先用ul跟li來定義menutree<br /><pre class="brush:html"><br /><div id="menubar_content"><br /> <ul><br /> <li><br /> <a href="#">item1</a><br /> <ul><br /> <li><span><img src="images/logo.png"></img>item1-1</span></li><br /> <li>item1-2<br /> <ul><br /> <li>item1-2-1</li><br /> <li>item1-2-2</li><br /> <li>item1-2-3</li><br /> </ul><br /> </li><br /> <li><br /> <a href="#"><b>item1-3</b></a><br /> <ul><br /> <li><a href="#"><b>item1-3-1</b></a></li><br /> <li>item1-3-2</li><br /> <li>item1-3-3</li><br /> </ul><br /> </li><br /> </ul><br /> <br /> </li><br /> <br /> <li><br /> item2<br /> <ul><br /> <li>item2-1</li><br /> <li>item2-2</li><br /> <li>item2-3</li><br /> </ul><br /> <br /> </li><br /> <li><br /> item3<br /> <ul><br /> <li>item3-1</li><br /> <li>item3-2</li><br /> <li>item3-3</li><br /> </ul><br /> <br /> </li><br /> <br /> </ul> <br /></div><br /></pre><br /><br />再來就是用jquery的語法來產生menu<br /><pre class="brush:javascript"><br />$(function()<br />{ <br /> $("#menubar_content").menu(); <br />});<br /></pre><br /><br />用法很簡單吧.. <br />這都歸功於menu強大又精簡的framework.. (y)popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com0tag:blogger.com,1999:blog-8260844445527494264.post-17751219438615057062010-07-13T13:24:00.008+08:002011-03-19T21:40:34.348+08:00用AS3上傳圖片<a href="http://popcornylu.blogspot.com/2010/07/as3-webcam.html">前一篇文章討論到要怎麼擷取webcam的照片</a>,但是照好的相片好像最常的應用是丟到server上吧。但是在AS3提共的<a href="http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/net/FileReference.html?allClasses=1">FileReferene</a>這個class只能提供讓使用者選檔案,而不能直接上傳我們擷取的bitmap。<br /><br />好在google中找到不錯的文章(<a href="http://marstonstudio.com/2007/10/19/how-to-take-a-snapshot-of-a-flash-movie-and-automatically-upload-the-jpg-to-a-server-in-three-easy-steps/">how to impress your friends by taking an image snapshot of a flash movie and automatically uploading the jpg to a server in three easy steps</a>)。作者寫了一個Helper來達到這個需求。<br /><br />首先你要把它的這個Helper Class抓下來,放在你的project當中。再來因為我們上傳可能需要encode成JPEG,所以我們需要用到<a href="http://code.google.com/p/as3corelib/">as3corelib</a>裡面的JPGEncoder轉成ByteArray。最後,你要在server端寫一個php或是server-side的程式去接你上傳的檔案。<br /><br />以下是我的flex程式<br /><pre class="brush:as3"><br /><?xml version="1.0" encoding="utf-8"?><br /><s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" <br /> xmlns:s="library://ns.adobe.com/flex/spark" <br /> xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="800" minHeight="600"><br /><br /><br /> <fx:Script><br /> <![CDATA[<br /> import com.adobe.images.JPGEncoder;<br /> <br /> import mx.controls.Alert;<br /> <br /> private var bitmapData:BitmapData = new BitmapData(640, 480); // the current capture data<br /> private var encoder:JPGEncoder = new JPGEncoder(80);<br /> [Bindable] private var isCapture:Boolean = false;<br /> <br /> public static const PATH:String = "http://your_url/";<br /> public static const FILE:String = "my.jpg";<br /> <br /> <br /> private function videoDisplay_creationComplete():void {<br /> var camera:Camera = Camera.getCamera();<br /> if (camera) {<br /> camera.setQuality(0, 100);<br /> videoDisplay.attachCamera(camera);<br /> } else {<br /> Alert.show("You don't seem to have a camera.");<br /> }<br /> }<br /> <br /> private function capture(): void { <br /> var bitmap:Bitmap = new Bitmap(bitmapData);<br /> <br /> bitmapData.draw(videoDisplay);<br /> bitmap new Bitmap(bitmapData);<br /> <br /> while(panel.rawChildren.numChildren > 0)<br /> {<br /> panel.rawChildren.removeChildAt(0);<br /> }<br /> panel.rawChildren.addChild(bitmap);<br /> isCapture = true;<br /> }<br /> <br /> private function update(): void {<br /> var byteArray:ByteArray = encoder.encode(bitmapData);<br /> var request:URLRequest = new URLRequest();<br /> var loader:URLLoader = new URLLoader();<br /> var parameters:Object = new Object();<br /> <br /> request.url = PATH;<br /> request.contentType = 'multipart/form-data; boundary=' + UploadPostHelper.getBoundary();<br /> request.method = URLRequestMethod.POST;<br /> request.data = UploadPostHelper.getPostData(FILE, byteArray, parameters);<br /> request.requestHeaders.push( new URLRequestHeader('Cache-Control', 'no-cache'));<br /> <br /> loader = new URLLoader();<br /> loader.dataFormat = URLLoaderDataFormat.BINARY;<br /> loader.addEventListener(Event.COMPLETE, onComplete);<br /> loader.addEventListener(IOErrorEvent.IO_ERROR, onError);<br /> loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);<br /> loader.load(request);<br /> }<br /> <br /> private function onComplete(event:Event): void<br /> {<br /> var loader:URLLoader = event.target as URLLoader; <br /> Alert.show("complete" + loader.data);<br /> }<br /> <br /> private function onError(event:Event): void<br /> {<br /> Alert.show("error");<br /> }<br /> <br /> ]]><br /> </fx:Script><br /> <br /> <mx:VideoDisplay id="videoDisplay"<br /> creationComplete="videoDisplay_creationComplete();"<br /> width="640"<br /> height="480" /> <br /> <br /> <mx:Panel id="panel" width="640" height="480" x="680" y="0"/><br /> <s:Button x="10" y="548" label="capture" click="capture()"/><br /> <s:Button x="100" y="548" label="update" click="update()" enabled="{isCapture==true}"/><br /></s:Application><br /></pre><br /><br />再來就是接收端php的程式<br /><pre class="brush:php"><br /><br /><?php<br />if ((($_FILES["file"]["type"] == "image/gif")<br /> || ($_FILES["file"]["type"] == "image/jpeg")<br /> || ($_FILES["file"]["type"] == "image/pjpeg"))<br /> && ($_FILES["file"]["size"] < 2000000))<br /> {<br /> if ($_FILES["file"]["error"] > 0)<br /> {<br /> echo "Return Code: " . $_FILES["file"]["error"] . "<br />";<br /> }<br /> else<br /> {<br /> echo "Upload: " . $_FILES["file"]["name"] . "<br />";<br /> echo "Type: " . $_FILES["file"]["type"] . "<br />";<br /> echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";<br /> echo "Temp file: " . $_FILES["file"]["tmp_name"] . "<br />";<br /><br /> if (file_exists("upload/" . $_FILES["file"]["name"]))<br /> {<br /> echo $_FILES["file"]["name"] . " already exists. ";<br /> }<br /> else<br /> {<br /> move_uploaded_file($_FILES["file"]["tmp_name"],<br /> "upload/" . $_FILES["file"]["name"]);<br /> echo "Stored in: " . "upload/" . $_FILES["file"]["name"];<br /> }<br /> }<br />}<br />else<br />{<br /> echo "Invalid file";<br />}<br />?><br /></pre>popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com0tag:blogger.com,1999:blog-8260844445527494264.post-39352993520957346202010-07-12T15:20:00.004+08:002011-03-19T21:40:22.347+08:00AS3 - 用Webcam來照相這個技巧很簡單,主要有三樣東西<br />1. Camera: 這個代表的就是某個Webcam元件。<br />2. VideoDisplay: 這個control可以attach一個Camera。也就是讓Camera的資料顯示在上面。<br />2. BitmapData: 這個Class可以用來包含我們截圖下來的bitmap data.<br /><br />當capture動作開始時,我們只要先產生一個BitmapData,並且用draw這個方法去把VideoDisplay裡面的Camera資料畫在BitmapData就好了。有了BitmapData後,我們用Bitmap把它包起來就可以放到讓何一個container裡面了。<br /><br />注意的是因為Bitmap並不屬於IUIComponent,所以我們要加他的話,必須要用rawChildren才可以加進它唷,這邊注意一下。<br /><br />下面是程式碼<br /><pre class="brush:as3"><br /><?xml version="1.0" encoding="utf-8"?><br /><s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" <br /> xmlns:s="library://ns.adobe.com/flex/spark" <br /> xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="800" minHeight="600"><br /> <fx:Script><br /> <![CDATA[<br /> import mx.controls.Alert;<br /> <br /> private function videoDisplay_creationComplete():void {<br /> var camera:Camera = Camera.getCamera();<br /> if (camera) {<br /> videoDisplay.attachCamera(camera);<br /> } else {<br /> Alert.show("You don't seem to have a camera.");<br /> }<br /> }<br /> <br /> private function capture(): void {<br /> var bitmapData:BitmapData = new BitmapData(320, 240);<br /> bitmapData.draw(videoDisplay);<br /> var bitmap:Bitmap = new Bitmap(bitmapData);<br /> while(panel.rawChildren.numChildren > 0)<br /> {<br /> panel.rawChildren.removeChildAt(0);<br /> }<br /> panel.rawChildren.addChild(bitmap);<br /> } <br /> <br /> ]]><br /> </fx:Script><br /> <br /> <mx:VideoDisplay id="videoDisplay"<br /> creationComplete="videoDisplay_creationComplete();"<br /> width="320"<br /> height="240" /> <br /> <br /> <mx:Panel id="panel" width="320" height="240" x="340" y="0" title="Hello"/><br /> <s:Button x="10" y="248" click="capture()"/><br /></s:Application><br /><br /></pre>popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com0tag:blogger.com,1999:blog-8260844445527494264.post-43996980737813479472010-07-09T16:52:00.009+08:002011-03-19T21:39:34.924+08:00Flex使用Json最近的project需要用Flex去跟webservice撈資料<br />而因為吐出來的格式是json..所以稍微研究了一下<br />下面是我做出的筆記<br /><br />1. 請去抓<a href="http://code.google.com/p/as3corelib/">js3corelib</a>,我們需要用到裡面的json library<br />2. 使用<code>JSON.decode()</code>,它會把json object轉換成as3的object..<br /><br />下面是一個sample code<br /><pre class="brush: as3"><br /><?xml version="1.0" encoding="utf-8"?><br /><s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" <br /> xmlns:s="library://ns.adobe.com/flex/spark" <br /> xmlns:mx="library://ns.adobe.com/flex/mx" <br /> xmlns:mycomp="*"><br /><br /> <fx:Script> <br /> <![CDATA[<br /> import com.adobe.serialization.json.*; <br /> import flash.net.*;<br /><br /><br /> protected function onClick(event:MouseEvent):void<br /> {<br /> var path:String = "http://your_url/";<br /> <br /> var ur:URLRequest = new URLRequest(path); <br /> var ul:URLLoader = new URLLoader();<br /> ul.load(ur);<br /> ul.addEventListener(Event.COMPLETE, onComplete);<br /> }<br /> <br /> protected function onComplete(evt:Event): void {<br /> var ul:URLLoader = evt.target as URLLoader;<br /> var obj:Object = JSON.decode(ul.data);<br /> lbl.text = obj.foo;<br /> }<br /> ]]><br /> </fx:Script> <br /><br /> <mx:HBox> <br /> <s:Button label="Get" click="onClick(event)"/><br /> <s:Label id="lbl"/><br /> </mx:HBox><br /> <br /></s:Application><br /><br /></pre>popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com0tag:blogger.com,1999:blog-8260844445527494264.post-40597059200263821212010-06-24T15:29:00.002+08:002010-07-11T15:58:05.024+08:00Flash debug..<div>目前好像比較好的方法是用flashbug這個firefox的plugin.</div>而flashbug本身是firebug的plugin...所以也要裝上firebug..<div><br /></div><div>plugin抓完...要debug flash當然也要裝debug版的flash player<br /><div>並且要抓debug版的flash player..</div><div><br /></div><div>都裝好後..</div><div>記得到</div><div>c:\users\yourname\mm.cfg</div><div>加上這兩行</div><div><div><span class="Apple-style-span" style="font-family:'courier new';">ErrorReportingEnable=1</span></div><div><span class="Apple-style-span" style="font-family:'courier new';">TraceOutputFileEnable=1</span></div></div><div><span class="Apple-style-span" style="font-family:'courier new';"><br /></span></div><div><div><br /></div></div></div>popcornyhttp://www.blogger.com/profile/17922275447374533899noreply@blogger.com0