2015年1月27日

讓iOS應用程式支援64-bit處理器的二三事

前言

近年來,新款行動裝置的處理器也逐漸轉為64-bit架構;在iOS設備方面,iPhone 5S, iPhone 6, iPhone 6 Plus, iPad Air, iPad Air2, iPad mini2, iPad mini3皆已採用arm64處理器[1]。

因此,Apple官方也宣布:2015年2月1日之後,新上架的App必須要支援64-bit處理器[2];而2015年6月1日之後,舊的App若要進行更新的話,也必須要支援64-bit[3]。

一般而言,撰寫高階語言時,通常應該心力專注於商務邏輯,程式可讀性與可重複使用性...等,而不太需要關注在與硬體架構的相關的事情;因為與硬體架構相關的事情應由編譯器與開發工具來處理。使用XCode也不外乎如此,理論上只要做以下簡單的設定即可支援arm64(事實上,在XCode 6預設即是以下設定):



然而實際上,使用不同的程式語言、編譯器與開發工具, 在不同的作業系統與硬體設備上開發應用程式,都有若干不同的細節需要注意;因次,程式設計師才必須對計算機結構也有所涉略。

而關於iOS應用程式開發的部分,Apple官方也寫了一份文件給iOS應用程式開發者: 64-Bit Transition Guide for Cocoa Touch

以下則是我在網路上比較常看到被提出來討論的:

(1) NSInteger vs. int

新手iOS程式設計師很常犯下混用NSInteger與int的錯誤:
NSNutableArray* myArray;
//...
unsigned int count = [myArray count];
因為在標頭檔裡,NSInteger與NSUInteger的確是這樣被定義的:
typedef int NSInteger;
typedef unsigned int NSUInteger;
因此,在只需撰寫32-bit app的情況時,這樣的確是不會有什麼問題。
但是,在撰寫64-bit app的情況時,可就不一樣了,編譯器會告訴你:
Implicit conversion loses integer precision: 'NSUInteger' (aka 'unsigned long') to 'unsigned int' 
原因很簡單,因為在標頭檔裡,完整的定義是這樣的:
#if __LP64__ || (TARGET_OS_EMBEDDED && !TARGET_OS_IPHONE) || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64
typedef long NSInteger;
typedef unsigned long NSUInteger;
#else
typedef int NSInteger;
typedef unsigned int NSUInteger;
#endif
說穿了,也真不是什麼高深的學問,不過就是:
NSInteger在32-bit環境下是int, 在64-bit環境下是long
如此而已。
只是在Apple強迫大家撰寫支援64-bit app之前,許多iOS程式設計師可能都不會去注意這個細節。
此外,在將NSInteger/NSUInteger格式化為字串時,以往在只有32-bit的環境下,可能會這樣寫:
NSInteger num = 1;
NSLog(@"num: %i", num);
但由於在64-bit的環境下, NSInteger/NSUInteger為long型別, 因此編譯器會提出警告;簡單的解決方式是手動將NSInteger轉型成long
NSInteger num = 1;
NSLog(@"num: %ld", (long) num);
或是使用z與t修飾子:
NSInteger num = 1;
NSLog(@"num: %zd", num);

NSUInteger num2 = 1;
NSLog(@"num2: %tu", num2);

(2) BOOL vs. bool

先看看下面這段簡單的例子,雖然正常情況下你不應該寫出這樣的程式:
BOOL b = 2;
if(b == YES) {
    NSLog(@"YES");
}
if(b == NO) {
    NSLog(@"NO");
}
你可以試著分別編譯出32-bit與64-bit的版本來執行, 結果可能出乎你意料:
32-bit版本,一個字都不會印出來;而64-bit版本, 則會印出YES
原因是32-bit環境下的BOOL是signed char,而非真正的bool型別。 熟悉C語言的朋友都知道,signed char允許存放-128~127。 而64-bit版本的BOOL是真正的bool型別,將非0值賦值給bool型別的變數,都將轉為1(true值)。
#if !defined(OBJC_HIDE_64) && TARGET_OS_IPHONE && __LP64__
typedef bool BOOL;
#else
typedef signed char BOOL; 
#endif
#if __has_feature(objc_bool)
#define YES __objc_yes
#define NO  __objc_no
#else
#define YES ((BOOL)1)
#define NO  ((BOOL)0)
#endif
簡單來說,BOOL型別的變數,應避免賦予YES與NO以外的值; 因此,對於正常使用BOOL型別的變數的程式設計師而言,其實不需要太擔心; 但若是因為知道BOOL是signed char而刻意作為他用,賦予了YES與NO以外的值的人,此時可要特別注意了;因為64-bit版本的BOOL是真正的bool型別,賦予了YES與NO以外的值,都將被視為YES

結論

以上簡單的例子,通常大多數人都不至於會犯;
但背後所隱藏的細節,卻很有可能被我們忽略,而在其他情況時犯下類似的錯誤;而這些錯誤可能難以被找出來。
因此,儘管是撰寫高階語言,對於編譯器、開發工具及硬體架構也應稍微有所了解。

Reference