2013年1月27日 星期日

iOS 使用 NSURLConnection 實作續傳的注意事項

  • 了解 http request/response header 的用法, 看實例比較好懂: Sample http range request session - Stack Overflow
  • NSURLConnection 連線 URL 所用的 scheme 為 http/https 時, 可放心在 callback 中將 NSURLResponse 往下轉型為 NSHTTPURLResponse, 這樣才能取得更多 http 特定的資訊 (如 http response header): iphone - When is a NSURLResponse not a NSHTTPURLResponse? - Stack Overflow
  • 依 SDK 6.0 NSURLConnection.h 裡所說的, cancel 之後之後仍有可能呼叫 callback
    The -cancel message hints to the loader that a resource load should be abandoned but does not guarantee that more delegate messages will not be delivered. If -cancel does cause the load to be abandoned, the delegate will be released without further messages. In general, a caller should be prepared for -cancel to have no effect, and internally ignore any delegate callbacks until the delegate is released.
    感覺上除了 fail 或 finished 兩個 callback 之外, 其餘情況無法保證 NSURLConnection 的生命週期已結束。呼叫 cancel 後不能接著呼叫 release/autorelease, 否則可能會造成 crash。這和目前官網 (iOS v5.0) 所說得不同, 使用 cancel 時要留意一下實際情況。有過一些 async IO 開發經驗後, 可以理解為何不方便「保證 cancel 立即有效」, 但至少提供一個 "didCancel" 的 callback, 這樣才知道什麼時候可以 release NSURLConnection。

iOS 實作 "Open in" 的注意事項

幾個重點

  • 使用 UIDocumentInteractionController, How to let your iOS app open files in OTHER apps. 有範例
  • 若是提供 view 給 UIDocumentInteractionControlle 的話, 其中 rect 可用 CGRectZero 表示選單顯示在左上角
  • UIDocumentInteractionController 初始化後沒有占有 reference count, 記得 retain 否則之後會 crash
  • 自己的 app 會複製一份 app 到別人的 app 裡, 然後別人的 app 開啟它自己的複製品
  • UIDocumentInteractionController.UTI 有可能偵測得不對, 看文件說它會自動偵測的樣子, 但用起來卻不是那麼一回事, 下載 foo.pdf, 卻沒有呼叫 iBooks。參考 The iOS 5 Developer's Cookbook: Documents and Data Sharing 填入自己用副檔名或 mime type 偵測的 UTI, 就有正常運作。

整合版本管理系統在 IDE 裡使用 vs. 直接使用版本管理系統

不論是使用命令列或視窗圖形介面的版本管理系統 (svn, hg, git, etc),我都偏好直接使用版本管理系統。這有許多好處:

  • 換開發環境時,不用重新學習一次。熟悉版本管理系統的功能愈多,重新學習的成本愈高。
  • IDE 內的 plugin 有可能是比較舊的版本

最近寫了一陣子 Android 和 iOS 程式後,對這個選擇更有信心。不論現在是用 vim + C++ 寫伺服器端 、用 vim + Python 寫網站、用 XCode 寫 iOS、用 Eclipse 寫 Android,通通都是一樣的方式使用版本管理系統,省了不少力氣適應不同的開發環境。

現今的 IDE 或編輯器都會自動偵測檔案更動時間,在別的地方用版本管理系統更動檔案,切回 IDE 或編輯器後會自動更新,不會編輯錯內容。讓這個選擇更無風險。

2013年1月21日 星期一

Programming in Objective 5e

上網查了一下 Objective-C 的入門書, 大部份人推薦《Programming in Objective-C》 (目前出到第五版) 還有讀 Apple 官方文件。可惜的是, 本書是針對沒學過程式語言的人, 暫時沒找到如《Python Essential Reference》那樣針對有經驗開發者的書 (見 學 Python 的入門書 )。

每天搭車時大概掃一下, 很快就讀完這本書了, 大部份情況是跳著讀範例程式碼, 有疑惑的話再看附近的描述。

最近讀書和開發的過程裡, 順手作的筆記如下:

多數情況對應已學過的概念到語法上即可。基本上 Objective-C 是 C 的 super set, 底層實作也是轉化為 C 的 struct 和指標, 所以滿容易猜中正確用法。

整體來說是滿有意思的語言, 令人意外的是它比 Python、Java、JavaScript等熱門「新」語言還早出現。考量到應用範圍, 除非要長期專職寫 iOS 的應用程式, 不然不會深入研究這個語言。接下要逐步熟悉 IDE、標準函式庫等項目, 這比熟悉如何寫出道地的 Objective-C 來得務實。

2013-01-25 更新

今天去天瓏才發覺出中文版了, 偶而還是要逛逛書店才行...

Objective-C 的 id, NSObject* 和 id<NSObject> 和 dynamic binding 的運作方式

相關資料:

摘要心得:

  • id 類似 void*, 不等於 NSObject*。像是 NSProxy 沒有繼承自 NSObject。
  • 實務上不太會用到 NSObject*, 而會用到 id<NSObject>, 因為我們關心它是否符合 NSObject protocol, 不關心是否繼承自 NSObject。
  • id 實際上是 struct objc_object 的指標, 這個 struct 只有一個欄位 Class isa。而 [NSObject alloc] 會存型別資訊到 isa 裡, 所以 runtime 可以正確得知型別為 id 的物件是否有提供呼叫的方法。
  • id<SomeProtocol> 中多提到 SomeProtocol, 是協助編譯時的型別檢查。編譯器有愈多資訊, 愈能在編譯時找到錯誤, IDE 也有機會提供更多協助。

2013年1月18日 星期五

XCode unit test 和 NSURLConnection

XCode 的 unit test

學習 test、寫 test、維護 test 和開發時執行 test 的等待時間都有成本。若使用的成本太高, 寫起來就更不划算了。初步使用 XCode 內建的 OCUnit 後, 覺得用起來不太划算。沒有 Python 和 Java 那種流暢的感覺。看了別人的討論, third-party unit test framework 還未出現救世主, 期待 XCode 5.x 會不會有友善的環境。

除單獨執行某組 test 或從上回失敗的 test 開始執行這類基本需求外, Python/Java 各有一大好處:

  • Python + nosetests: 可以自行搜集目錄下的 unit test
  • Java + Eclipse: 可以 100% 直線式進行 TDD: 寫 test 呼叫目標 class 和 method -> 用 Eclipse 產生空的目標 class 和對應的 method -> 實作 method。這種寫法頗痛快的

而 XCode + OCUnit 令人意外的只能執行全部 test。介面的部份待用久一點後再來評論。

unit test 與非同步操作

unit test 測到如 NSURLConnection 的非同步操作時, 記得在 unit test 裡呼叫

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]

這樣才有分配時間執行非同步操作, 不要傻傻的等, 想說怎麼 NSURLConnection 的 callback 都沒被呼叫到? 細節見The Run Loop In Cocoa Unit Tests

2013-01-25 更新

今天翻閱Test-Driven iOS Development (Developer's Library) 才知道在可以選擇要執行的 test 項目, 這本書薄又滿有料的, 很快就可以翻完。長期開發 iOS 又對 TDD 有興趣的話, 看看應該會有幫助。依我目前作的事來說, 時間先花在熟悉 XCode 和 framework 會比較划算就是了。

NSMutableDictionary 的限制、Category 的限制和 Associative Reference

為了確保 key 的內容不變, NSMutableDictionary 在加入 key/value pair 時, 會複製一份 key。這個決定可以避免許多 bug, 不過遇到不支援 NSCopying 的物件會有些麻煩。

比方說在使用 NSURLConnection 時, NSURLConnection 的 callback 會傳回 connection 做為辦別目前是那個 connection 的 callback。問題在於, 若允許同時下載重覆的 URL 並且需要 NSURLConnection 額外的資訊 (比方說自己暫存用的檔名), 要如何取得額外資訊呢?

一個直覺的作法是用 NSMutableDictionary: 以 NSURLConnection 為 key, value 存放對應的額外資訊。然後就踩到 NSMutableDictionary 要求 NSCopying 的雷。

變通作法是使用 Category 幫 NSURLConnection 加上 "connectionID", 這樣就可以取得自定的辨別值作為 NSMutableDictionary 的 key。不幸的是, Category 不能新增 member field, 所以不能直接用這招。Category 不允許新增 member field 是合理的決定, 因為加了 member field 以後, 要在什麼時候 release 它們? 用 Category 覆寫既有的 dealloc 不是個好主意。

回想一開始的需求, 若還是想在 Category 內加 member field 怎麼辦? 針對此點的變通作法是使用 Associative References

另一個可行的笨方法是建兩個 NSMutableArray, 一個存 NSURLConnection, 一個存額外資訊。要取用額外資訊時, 先找到 NSURLConnection 的 index, 再用此 index 取出額外資訊。在同時擁有的 connection 數不大的時候 (比方說 <100), 這個作法不會有效能問題。

更新

Scott 提到這個情況使用繼承更簡單, 我完全忘了可以繼承標準函式庫 class, 這個作法比上述作法好。另外提到 C 程式另有使用指標位置位移的技巧取得額外資訊, 附上 Scott 的原文:

C 程式中解這種『從既有 data type 查額外資訊』,有個不太直覺的解法, container_of() : http://linuxkernel51.blogspot.tw/2011/02/how-containerof-macro-works-example.html

struct MyCustomType {
    int misc;
    struct ExistingType m0;
    void *data;
};
 
void my_callback(struct ExistingType *e)
{
   struct MyCustomType *p = containter_of(e, struct MyCustomType, m0);
   /* access p->data ... */
}
 
/* See http://www.kroah.com/log/linux/container_of.html
   for how the container_of() macro is implemented. */

這個片語是在 MyCustomType 內嵌一個 ExistingType,然後當你確定手上的 (ExistingType *) 來源時就能丟進 cotainer_of() 查回 MyCustomType。有點 『既然我用的程式語言支援 pointer arithmetic 那我就盡量用』的感覺。Linux kernel 內的連結串列就是用這個片語寫的 http://kernelnewbies.org/FAQ/LinkedLists

2013年1月16日 星期三

Objective-C category 和 informal protocol

在查 NSURLConnection 的時候, 看到一堆重要的 method 被 deprecated 了, 感到相當困惑。讀了這篇說明才明白, 原來東西都在, 用法其實也沒什麼變, 只是宣告的方式變了。

首先要理解 Objective-C 的語法 Category:

@interface SomeClass (SomeCategory)
...
@end

在 ... 宣告的方法, 是對 SomeClass 的擴充, 這組擴充的名稱為 SomeCategory。

Category 可用在包含標準函式庫 (Foundation) 內的任何 class。這有什麼好處呢? 比方說你需要一個比較特別的 Set 操作, 像是 [mySet uniformRandomObject] 傳回 mySet 內隨機的一個物件。不論是用組合還是繼承的方式訂新的 class, 都不太方便。這時用 Category 就很方便了:

// NSSet+RandomOps.h
@interface NSSet (RandomOps)

-(id)uniformRandomObject;

@end

// NSSet+RandomOps.m
@implementation NSSet (RandomOps)

-(id)uniformRandomObject
{
   ...
}

@end

再來, Protocol 相當於 Java 的 interface, 也就是只有宣告但不帶實作的介面。另一方面, 只有宣告 Category 卻沒有提供實作的宣告, 就稱為 Informal Protocol。通常會宣告在 NSObject 上, 這樣任何客戶碼都可以實作需要的 method 而不用繼承其它 class。 NSURLConnection deprecated 的那些方法就是採用此方法。在 iOS 4.3 to iOS 5.0 API Differences 搜尋 "NSURLConnection.h" 可看到多數 callback 從 NSObject 裡移到新的 protocol NSURLConnectionDelegateNSURLConnectionDataDelegateNSURLConnectionDownloadDelegate 裡面。

Objective-C 2.0 以前 protocol 沒有 @optional 的語法。若希望 delegate 可以選擇性實作 callback methods, 只能使用 Informal Protocol。有了 @optional 語法後, 比較不需要用到 Informal Protocol。

Btw, Objective-C 同時有 static typing 和 dynamic typing 的效果, 還滿神奇的。不知當年訂這語言的人在想什麼, 雖然我喜歡使用 Python, 不過更偏好 static typing, 待用更多 Objective-C 後再來體會這種組合的優缺點。

2013年1月12日 星期六

Objective-C memory management

主要參考資料是 Programming in Objective-C 5e ch17, 網路上這篇也滿清楚的。

Objective-C 提供三種管理記憶體的方式:

  • automatic garbage collection: 只有 Mac OS 提供, 且在 OS X 10.8 後宣告 deprecated, 所以不該繼續使用。
  • manual reference counting: 透過一套訂好的 API 和規定函式名稱代表的語意, 讓開發者比較容易管理 reference counting。
  • automatic reference counting: 透過 compiler 生成 reference counting 相關的程式碼, 開發者不用那麼留意 reference counting, 看起來相當美好, 作者極力讚賞這個機制。

Manual Reference Counting (MRC)

NSObject 提供 retain 和 release 兩個方法來 +1 / -1 reference count。所有物件都繼承自 NSObject。開發者要記得如下的規則:

  • 擁有物件的人記得要 +1, 不用時記得要 -1
  • count 為 0 時會呼叫 dealloc, 開發者可自訂 dealloc 釋放自己額外擁有的物件
  • 誰配置這物件, 就是它的初始擁有者
  • 從 copy, mutableCopy, alloc, new 取得的物件, 已經 +1 過了, 不用時記得呼叫 release。有些文章指出有呼叫 init* 開頭的方法表示有 +1, 反之則沒有。像是 [[NSString alloc] initWithUTF8String:X] 之於 [NSString stringWithUTF8String:X] 。這個說法不太正確, 應該是看 alloc 不是看 init。[NSString stringWithUTF8String:X] 是 class method, 不是 instance method, 所以 caller 自己沒有呼叫到 alloc, 自然也沒義務呼叫 release
  • 搭配 @autoreleasepool 可使用 [X autorelease] 先標記「之後記得 -1」。好處是呼叫方法 A 會傳回新物件時, 可在回傳前呼叫 autorelease, 表示方法 A 沒有擁有這個新物件, 且不會讓新物件在傳回前就被消滅。在 @autoreleasepool 的區塊結束時, 會讓所有呼叫過 autorelease 的物件呼叫 release。所以 [NSString stringWithUTF8String:X] 就是呼叫 alloc, 接著呼叫 autorelease, 再傳回新的 NSString
  • 可用 @property(nonatomic, retain) 表示設值的時候, 順便在目標物件上呼叫 retain。第二個參數預設是 assign, 表示沒有只有設 object reference, 沒做額外動作 (如呼叫 retain)

另外兩個和記憶體相關的要點是:

  • 對 nil 呼叫任何方法, 相當於 NOP, 不用擔心程式會掛掉。所以 [nil release] 也是合法的。
  • 從 heap 配置的資料都會初始化為 0, 所以若 instance fields 有指標的時候, 其值皆為 nil。

整體來說, 清楚規則後寫起會滿順手的, 不過需要點時間理清這些規則, 還有因為不是由 compiler 或 runtime 強制處理, 犯錯時不易察覺。

比方說使用 @property(nonatomic, retain) str 時, 在 initStr:s 裡要怎麼設才對?

@interface MyClass : NSObject

@property(nonatomic, retain) NSString *str;

@end


@implementation MyClass

@synthesize str = _str;

-(NSString*) initStrA:(NSString*)s
{
    // 錯誤, 沒有 +1, 之後需要加一行 [_str retain];
    _str = s;
    return self;
}

-(NSString*) initStrB:(NSString*)s
{
    // 錯誤, 多加了一次
    self.str = s;
    [_str retain];
    return self;
}

-(NSString*) initStrC:(NSString*)s
    // 正確, 呼叫到 synthesize 產生的 setter, 
    // 會在設值前呼叫 [s retain] 和 [_str release], 再做 _str = s
    self.str = s;
    return self;
}

備註

寫這篇時手邊沒有 Mac 環境可編譯上述的程式, GNUStep 在 Linux 下編譯的結果會有些錯誤, 但是錯誤來自於對 @property 和 @synthesize 的支援程度不同於 XCode 4.5。也因為這樣碰巧明白 Objective-C 可能不方便跨平台, 在 Mac OS、iOS 以外的平台, 寫起來可能不會很愉快。

Automatic Reference Counting (ARC)

編譯時使用 ARC 的情況下, 除了 compiler 會在 "=" 出現時自動幫右邊 +1, 左邊原始的值 -1 外, compiler 也會知道用 copy, mutableCopy, alloc, new 等方法取得的物件已 +1, 而幫你管對記憶體。除此之外, 還多了 strong/weak pointer 的用法。

想像有個 UI 的 view A, 裡面有個 sub view B (比方說一個視窗裡面有張圖)。A 自然會有 B 的 reference, 而 B 有 A 的 reference 的話, 比較方便執行一些 callback 動作。於是, A 和 B 互指對方 (互相幫對方 +1), 即使沒有任合其它物件用到 A 和 B 時, A、B 仍然不會被回收。

weak pointer 的特色是它不會列入計數, strong pointer (預設) 才會計數。所以, 若 A 用 strong pointer 擁有 B, 而 B 用 weak pointer 擁有 A。那麼, 沒有物件擁有 A 的時候, A 會被回收。此時, weak pointer 的值會自動設為 nil, 所以之後 B 在作操作時可檢查是否為 nil 了解被回收掉了, 或是不檢查直接操作 weak pointer, 也不會讓程式掛掉。當然, 以這例子來說, A 的 dealloc 應該會減少 B 的計數, 若沒有其它物件擁有 B 的話, B 也會被回收掉。

心得

像這樣用 compiler 協助生成程式碼管理 reference counting 頗妙的, 同時滅少 runtime 複雜度和開發者的負擔 (compiler 的心聲: 都沒人在意 compiler 的工作負擔啦...)。讓我對 reference counting 有不同過去死板的認知

此外, 多了 strong 和 weak pointer 的語意, 減少產生 circular reference 的可能性, 並降低物件消滅繼而回收擁有權的難度。舉例來說, 若物件 O 註冊「當 S 有變化時, 記得呼叫 O」。那麼, S 使用 weak pointer 儲存 O 的話, 可以讓 O 在不需擁有 S 的情況下, 自由地於任何時段滅亡, 只要 S 在呼叫 O 時先檢查 pointer 是否變為 nil 即可 (multi-thread 的話, 仍需要 lock 輔助就是了)。

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...