顯示具有 iOS 標籤的文章。 顯示所有文章
顯示具有 iOS 標籤的文章。 顯示所有文章

2015年7月19日 星期日

iOS kCFErrorHTTPSProxyConnectionFailure 和 Android ERR_TUNNEL_CONNECTION_FAILED 的解法

iOS kCFErrorHTTPSProxyConnectionFailure (錯誤代碼 310) 的意思是 proxy 不允許 HTTPS 的連線,可能是 client 用 https 連往 443 以外的 port, 然後 proxy 設定不允許這樣的連線。解法是改在 443 port 執行用 https 的 web server, 或是 client 不要使用 proxy。

我用同樣的設定 (在 port 443 以外跑 https + client 用 proxy) 改測 Chrome on Android, 結果顯示的錯誤代碼是 ERR_TUNNEL_CONNECTION_FAILED, 也許這個代碼有用在其它情況。遇到的時候也可以看看是否和這有關。

2014年4月25日 星期五

過濾 iOS log 的方法

平常開發 Linux 程式需要即時觀察 log, 又有太多 log 要過濾的時候, 會用

$ tail -f LOG_FILE | grep PATTERN  # 只留符合 PATTERN 的 log

或是

$ tail -f LOG_FILE | grep -v PATTERN  # 去掉符合 PATTERN 的 log

command line 的 pipe 可以無限接, 方便做很多延伸運用。

開發 Android 的時候可以透過命令列的 adb logcat, 或是 Eclipse 也有提供介面過濾 log。

開發 iOS 時比較困擾, 通俗是用 NSLog 寫到 console, 但是沒有找到用 XCode 過濾的方法。

轉念一想, 既然 iOS 裝置接上 XCode 後可以看到過去的 console log, 表示 log 存在檔案裡。直接取 log 的原始檔案, 比照在 Linux 的習慣直接對檔案操作應該比較簡單。找了一陣子找到檔案放在 $HOME/Library/Application Support/iPhone Simulator/7.1/Library/Logs/system.log, 接著套上在 Linux 的習慣, 就方便多了。

2014年2月26日 星期三

如何找出 iOS crash log 中的 symbol name

從 Sam 那看到的參考文章: 别用symbolicatecrash来解析crash log了 - Wonderffee's Blog

若 binary 是別人產生, crash log 也是別人產生時, 可以照以下的方式對回 symbols:

  1. 取得產生 crash log 對應 app binary 的 archive 目錄下的 dSYMs 目錄。
  2. 在命令列下輸入: $ mdimport dSYMs。
  3. 將 crash log 存成 xxx.crash (副檔名必須是 .crash)。
  4. 打開 XCode 的 Oraganizer, 點選 Library 下的 Devices Logs, 點選 Import, 選擇 xxx.crash。

結果會顯示在畫面右側。

2013年12月25日 星期三

了解 2D Graphics Library 的基本知識

有很多套不同的 2D Graphics Library, 像是 Linux 下的 Cairo, OS X 和 iOS 下的 Quartz 2D 還有跨平台的 Skia, 但它們的概念應該差不多, 若看不懂其中一套, 可以參考別套的文件再回頭理解 (特別是 iOS 的文件最齊)。這篇記錄看了那些文件協助我了解 2D Graphics Library 的基本知識。

Cairo Tutorial

Cairo Tutorial 說明 Cairo 的架構, 配合圖示和簡短的範例程式, 相當實用。

Cairo 的架構分成: source, mask, destination, context。可以想像 destination 是一塊畫布 (pixel buffer), 可能對應到 bitmap、螢幕、PDF 或印表機。使用 Cairo API 設定好 source (如顏色、漸層模式 (gradien) 或另一個畫布) 和 mask, 再決定繪圖的動作, 就會將 source 經過 mask 的結果複製到 destination。

context 管理目前設定的各種屬性, 若有用過 OpenGL 或其它繪圖函式庫, 應該不會對 context 的概念感到陌生。繪製的動作會參考 context 內設定的屬性。

整體來說, 使用 2D Graphics Library 就是先產生一個 pixel buffer (Cairo 稱為 cairo_surface_t), 然後產生一個包含 pixel buffer 的 context (Cairo 稱為 cairo_t)。接著設定 context 屬性、source、mask, 然後決定繪圖動作 (如 fill、stroke)。

Path

Cairo Tutorial 有介紹 path 是什麼。Cocoa with Love: 5 ways to draw a 2D shape with a hole in CoreGraphics 提到如何用 path 畫出不同的填色效果。用同一個例子的不同作法說明為何需要 non-zero winding rule 或 even-odd rule。

Blending

參考 Porter/Duff Compositing and Blend Modes 了解各種 blend mode 的計算方式和圖例, 另外 CGContext reference 裡的 CGBlendMode 有同樣的說明。表中沒有 "source over", 因為 kCGBlendModeNormal 的效果就是 source over。

2013年7月19日 星期五

在 iOS 刪除 App 後不會刪除 keychain 內的資料

不過理論上 App 不能存取別其它 App 的 keychain 資料, 所以這勉強不算安全漏洞吧。若希望使用者重新安裝 App 時, 不會用到之前存在 keychain 的資料, 可在安裝後第一次啟動時先刪除 keychain 資料:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    //Clear keychain on first run in case of reinstallation
    if (![[NSUserDefaults standardUserDefaults] objectForKey:@"FirstRun"]) {
        // Delete values from keychain here
        NSArray *secItemClasses = @[(__bridge id)kSecClassGenericPassword,
                               (__bridge id)kSecClassInternetPassword,
                               (__bridge id)kSecClassCertificate,
                               (__bridge id)kSecClassKey,
                               (__bridge id)kSecClassIdentity];
        for (id secItemClass in secItemClasses) {
            NSDictionary *spec = @{(__bridge id)kSecClass: (__bridge id)secItemClass};
            SecItemDelete((__bridge CFDictionaryRef)spec);
        }
        [[NSUserDefaults standardUserDefaults] setValue:@"1strun" forKey:@"FirstRun"];
    }

    //...Other stuff that usually happens in didFinishLaunching
}

參考資料:

2013年7月14日 星期日

使用 iOS notification 的好處

先前在《iOS delegation vs. notification》提到我偏好 delegation, 這篇稍微修正一下對兩者的觀點。

開發 iOS 和 Android 以前我只有寫 Web 的經驗, 沒有寫圖形應用程式的經驗 (硬漢工程師就是要用 CLI 啊!!)。之前只覺得開發 Web 很容易上手, 不過跨瀏覽器很煩。最近多了一些 iOS 開發經驗, 才體會到為什麼 Web 這麼好寫, 個人認為的原因有二:

換句話說, 寫網頁可以輕鬆得知 model 狀態變化, 得知變化時還可以輕鬆地改變任何相關的 UI 元件。

反觀寫圖形應用程式時, 離上述兩點相當地遙遠, 寫起來綁手綁腳的。或許是出於類似的需求, iOS 有提供 NSNotificationCenter 協助 observe 和 dispatch event。

於是, 透過 NSNotificationCenter 這個全域變數, 我們可以

如此一來, 減輕不少實作負擔。不過如同 When to use Delegation, Notification, or Observation in iOS 所言, notification 過於彈性會造成一些問題。個人認為配合一些規範, 像是使用 header 檔定義 notification name 和 user info 的 key, 不要直接用 string, 應該是不錯的選擇。

備註:

2013年7月8日 星期一

iOS UIView 除錯

Refernece: Visual View Debugging | Cocoanetics

這篇實在是太實用了, 備忘一下文中提到的技巧。

取得 view 資訊

NSLog(@"%@", view);

直接輸出 UIView 即可, 不用自己笨笨的輸出 frame 之類的資訊。

輸出 view 的巢狀結構

NSLog(@"%@", [view recursiveDescription]);

[UIView recursiveDescription] 是 private method, 不過 Objective-C 有 dynamic typing 的特性, 真的要呼叫的話, 系統也不會擋你, 只是編譯時有個 warning 而已。這功能相當實用, 可以確保相關的 view 是否有加進去, 了解 sub views 之間的順序 (後面的 view 會蓋掉前面的 view)。

在多個關鍵處輸出 recursive description 後, 才發覺我在某處不小心刪掉某個 view 沒加回去。若沒有這項功能, 執行後只知道沒出現這個 view, 到底是放錯位置、沒放進去或被覆蓋掉, 完全是一無所知啊。

畫出 view 的外框

view.layer.borderColor = [UIColor redColor].CGColor;
view.layer.borderWidth = 1.0;

使用上述程式的前置動作為:

  • linking 加入 QuartzCore.framework
  • 程式加上 #import <QuartzCore/QuartzCore.h>

2014-07-23 更新

  • [UIView recursiveDescription] 會顯示本身整層的結構, 若想看更上層的結構, 要往上存取 superview。但在 lldb 中打 po view.superview.superview 會失敗, 原因是 view.superview 傳回 id, 而 id 沒有型別, 無法直接呼叫 superview。所以要寫成 po ((UIView*)view.superview).superview。一但取得目標 UIView 的位置後 (例如 0x12345678), 再來就可以用 po [(UIView*)0x12345678 recursiveDescription] 得到目標的結構。
  • 承上, 更方便的作法是在 app 產生 UIWindow 的時候設中斷點, 取得 UIWindow 的位置 (例如 0x87654321), 之後隨時想看目前整個畫面的結構, 就按 XCode 的「暫停鍵」中斷程式進入 lldb, 再用 [(UIView*)0x87654321 recursiveDescription] 取得目前畫面的 UIView 結構。很快地了解 UIView 結構後, 要了解程式行為就容易多了。而且從 recursiveDescription 得知所有 subview 的位置後, 也可以配合轉型直接輸出各個 view 的其它屬性, 像是 [(UIScrollView*)0x12341234 zoomScale]。

2013年6月27日 星期四

XCode 新增子專案編譯 static library 的方法

這種事沒記下來立即就忘了, 我是用 XCode 4.6。

新增子專案

  1. 在 Project Navigator 點專案 A
  2. 點左下角的 '+', 選擇加新專案, 選擇使用 static shared library, 完成剩下的步驟
  3. 點專案 A: Build Phases -> Target Dependencies -> 加入專案 B 的 static shared library
  4. 點專案 A: Build Phases -> Link Binary with Libraries: 加入專案 B。若專案 A 之上有專案 M, 這步要加在專案 M

雜七雜八問題

  • 找不到 header: 在 Build Settings 搜尋 header 會找到 Header Search Paths 和 User Header Search Paths, 記得填入正確的值。可以對一下編譯的原始訊息了解設下去轉換成什麼東西。這裡的路徑是相對於專案位置的。
  • 連結失敗, 找不到 static shared library 內的 symbol: 確定 Link Binary with Libraries 裡面有加到這個 library, 還有有在正確的 project 內設 Link Binary with Libraries。
  • 連結失敗, static shared library 內有 undefined symbol: 注意在 Link Binary with Libraries 裡和其它 libraries 的順序。同樣地, 對一下編譯的原始訊息會比較清楚情況。
  • static shared library 一直是紅字: google 一下有人說疑是 XCode bug, 在實體裝置上編譯過後, 就沒有紅字了。我試的結果確實如此。
  • 找不到 C++ STL 的 symbol: 錯誤訊息像這樣:
    std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::__init(char const*, unsigned long)", referenced from:
    檢查 Build Settings -> C++ Standard Library, 這裡有三個選項:
    • libstd++ (GNU C++ standard library)
    • libc++ (LLVM C++ standard library)
    • Compilation default (be consistent)
    記得所有專案要設成一致的選項, 新增專案預設是 libc++。

結論是先有基本觀念, 出錯時比較知道怎麼找原因。雖然一樣是 google 關鍵字, 至少讀搜尋結果時, 知道應該優先讀那些文章, 效率會好很多。再來是盡量在 Build Settings 和 Build Phases 裡找和編譯以及連結相關的參數。

2015-06-09 更新

XCode 6 新增專案的方式變了,改成在 Project Navigator 點專案 A 然後按右鍵選 "New Project"。然後選 "Framework & Library" -> "Cocoa Touch Static Library"。

2013年4月5日 星期五

在 linux 和 iOS 上抓 memory leak 和 heap profiling 心得

問題描述

首先要定義一下什麼是「memory leak」。它的正式定義是指「程式弄丟了未釋放空間的位置」, 之後無法使用這塊空間也無法釋放它。另一方面, 程式沒有弄丟位置, 但是配置的空間愈用愈大, 或是用完後沒有歸還, 導致記憶體不足。若要應付第一種情況, 可用 memory leak 作為關鍵字找相關討論; 第二種情況要用 memory profiler 或 heap profiler 了解記憶體用去那裡, ( 感謝 Scott 告知 )。

就我自己少量的經驗來說, 第二種比較棘手, 第一種通常是粗心, 找到後補上 free/delete 即可。但第二種可能和程式配置和運用記憶體的策略有關。我遇到的問題都是第二種。若程式有針對不同區塊自己管記憶體的話, 比較方便日後植入統計程式找出瓶頸。順道一提, 如同 TDD 的精神, 在設計程式架構時就考慮測試, 會讓程式更易測試、維護和除錯。在設計架構時就考慮除錯, 也會讓程式更易維護和除錯。

在 Linux 上找 memory leak 和 profile heap

在 Linux 上試了兩套工具 mtrace 和 valgrind。mtrace 滿簡單的, 可用來抓 C 的 memory leak, 但不適用於 C++, 原因是 mtrace 在 malloc/free 動手腳, 可以找出那一行呼叫 malloc 後沒呼叫 free。但是 C++ 的情況是 libstdc++ 呼叫 malloc, 而我們有興趣的是誰呼叫了 new。

後來改試 valgrind 效果還不錯。valgrind 是多個除錯工具的集合體, 我試了兩個工具:

  • memcheck: 抓 memory leak 和記憶體讀寫錯誤。
  • massif: profile heap 的用量, 可看到不同時間 heap 由各函式配置的比例。

使用 valgrind 的代價是程式會慢個 20 倍左右。但是用工具協助檢測問題, 99% 的情況會比自己憑空猜想來得準。還是有些耐性使用 valgrind 會比較快找到問題源頭。

memcheck

$ valgrind --tool=memcheck --leak-check=full PROG PROG_ARG ...

跑完後會輸出結果在 console。建議先有些耐心看完官網文件。對 memcheck 的報表有疑問的話, 可參考 "Understanding Valgrind memory leak reports"。重點是 possibly leak 表示 valgrind 無法判斷它是 still reachable 或 definitely leak。still reachable 可能是某些函式庫配置自己一塊 memory pool 來用, 程式離開時才會釋放, 所以嚴格說起來不是 memory leak。在這個階段, 我們關心的是 definitely leak。

massif

$ valgrind--tool=massif PROG PROG_ARG ...

跑完後會輸出結果在 massif.out.PID。然後執行 ms_print 看結果

$ ms_print massif.out.PID

建議先有些耐心看完官網文件。我試了許多參數想抓到程式離開前的 detailed snapshot 都徒勞無功。最後發覺用 vgdb 意外的簡單。

用法如下:

  1. 先執行 valgrind
  2. 在另一個 shell 下執行 vgdb detailed_snapshot /path/to/snapshot
  3. 閱讀當下的 snapshot: ms_print /path/to/snapshot

valgrind 也可以和 gdb 一起互動, 沒想到會在這個機會下試用到 gdb 的 remote debugging 功能。

經 Scott 告知, 除了用 ms_print 看文字報表外, 也可用 Massif Visualizer 看結果。這工具看起來滿不錯的, 先備忘在這裡。

另外補充一下 Linux 下記憶體用量的意思。平常我們是看 ps 回報的 VSZ 和 RSS, 但兩者其實都不夠準確, 用 pmap -d PID 看最後一行輸出的 private writable memory, 可能比較適合。

在 iOS 的找 memory leak 和 profile heap

就用 Mac OS X 附的 Instrutments, 使用 Leak 或 Allocations, 不用看文件即可輕鬆搞定! 全圖形介面操作, 還有詳細的報表。要看各 functions 用掉 heap 記憶體的比例的話, 在 Allocations 裡點 Statistics -> Call Tree。

同事用過 Instruments 後, 甚至說「真想將 Linux 的程式 port 到 Mac, 這樣才可以用 Instruments」, 由此可知 Instruments 有多麼地好用。

2013年3月3日 星期日

Core Data lightweight migration

上份工作需要階段性處理大量資料, 當時有不少 database schema 與 data migration 的經驗, 雖然我不怎麼喜歡 Django 的 ORM, 不過配合 South 做 migration 到是滿方便的。

最近用 Core Data 不幸地在發佈測試程式後發覺需要增加欄位, 查了一下作法, 所幸 Core Data 有針對簡單的情況提供自動化 migration, 相當輕鬆, 簡記用法如下:

iOS delegation vs. notification

初始使用 iOS 的 NSNotificationCenter 覺得頗不錯的。UI 物件需要從一個遙遠的物件裡取得資料改變的狀態, 當初沒考慮好這個需求, 不太方便傳遞資料。這時用 notification 解套就容易多了。但是隱約覺得 notification 的用法不太妙, 查到這篇 When to use Delegation, Notification, or Observation in iOS 分析得很清楚, 摘要重點如下。
使用 notification 的缺點 (或說特色) 是:
  • notification 是非同步呼叫, 發送 notification 和接受 notification 在不同的 runloop iteration 裡, 不易除錯。
  • notification 的發送和註冊採用字串做為 key, 無法在編譯時期找到錯誤。執行時才會察覺「好像沒呼叫到」
另一方面, delegation 透過定義使用端的介面 (iOS 術語為 protocol), 運作的特點為:
  • 透過函式呼叫通知事件, 方便除錯
  • 可在編譯時期確保提供服務者和用戶端有正確串在一起
  • 除通知發生事件外, 可讓客戶提供實作供提供服務者使用
原文有提及更多項目, 不過對我來說最要緊的是「是否容易除錯」。從這個角度來看, 使用 delegation 遠比 notification 安全。所以可以的話, 盡量避免使用 notification 較好。

2013-03-03 更新

經 Sam 指正, 發覺 notification 預設是同步呼叫, 在 backtrace 裡可找到發出點和接收點。這樣就沒那麼難除錯了。重看一遍該篇文章, 作者提的除錯困難, 可能是指用太多 notification 混在一起的情況。

兩者的主要差別應該是介面的強度。介面規範強, 編譯時期自然抓到較多錯誤, 寫起來也比較麻煩一些。其它的小細節如「少了 observer 訊息、一對多 (多對一) 架構」等, 則是設計介面或使用時可補救的。

2013年2月9日 星期六

iOS crash 除錯小技巧

參考文章:

摘錄重點:

  • SIGABRT 比較好解, 是 framework 偵測到有異常, 能更接近問題的源頭; EXC_BAD_ACCESS 發生在存取記憶體出錯, 離案發現場可能有一段時間
  • 記得在 breakpoint 的輔助畫面君上 "Exception All + Break on Throw" 的 breakpoint, 可在有 exception 時看到 backtrace
  • edit scheme -> Diagnostics -> Enable Zombie Objects: 有機會在存取到已 release 的物件時, 當下抓到錯誤, 避免到後面出現不知所已的 EXC_BAD_ACCESS

之前用了 "Break on Throw" 和 "Enable Zombie Objects" 後, 減少了不少除錯時間。

2013年2月2日 星期六

Objective-C++ 使用 C function 的注意事項

寫 Objective-C++ (*.mm) 的時候, 記得規則和 C++ 一樣, 函式名稱會有name mangling。呼叫純 C 的函式時, 要留意編譯 C 的原始碼時, 是用 C 或 C++ 的方式編譯。若 object file 是用 C 的方式編譯, 引入的 header 檔要加上 "extern C" 的語法。

話說我是用 nm 看編出的 static lib 和呼叫它的 object file 時, 發現有兩個長得很像但不一樣的函式名稱, 才想起這件事。

相關文章:

XCode 編譯連結 static library 的注意事項

最近遇到兩個狀況

  • link 執行檔時, 找不到 static library 內用的 symbol
  • link 執行檔時, 找不到 Core Data 用的 schema (*.momd)

重新回憶《解決 undefined symbol / reference》, 想到 Mac OS/iOS 也是和 Linux 類似的架構, 原理應該一樣。上網搜了相關文章, 得到以下的解法:

  • static library 只能含有 object file, target 為 static library 的時候, 設定 Build Phases 裡的 Link Binary With Libraries 沒有意義。應該要設 application target 的 Link Binary With Libraries, 因為是建立 application (unit test, etc) 時才會 link libraries
  • 同上, static library 不能含有其它 resource file, 要放在其它 target 的 bundle 裡, 然後 static library 裡的程式讀另外的 bundle。

參考資料:

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, 就有正常運作。

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

在 Fedora 下裝 id-utils

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