終究還是來了。Apple下發了支援64位的最後通牒:
As we announced in October, beginning February 1, 2015 new iOS apps submitted to the App Store must include 64-bit support and be built with the iOS 8 SDK. Beginning June 1, 2015 app updates will also need to follow the same requirements.
早應該做的適配終於要開始動工了,苦了64位的CPU執行了這麼久32位的程式。前段時間公司專案完成了64-bit包的適配,本沒那麼複雜的事被無數不標準的老程式碼攪和的不輕,總結幾個Tip共勉。
Tips
拒絕基本資料型別和隱式轉換
首當其衝的就是基本型別,比如下面4個型別在32-bit和64-bit下分別是多長呢?
1 2 3 4 |
size_t s1 = sizeof(int); size_t s2 = sizeof(long); size_t s3 = sizeof(float); size_t s4 = sizeof(double); |
32-bit下:4, 4, 4, 8
;64-bit下:4, 8, 4, 8
(PS: 這個結果隨編譯器,換其他平臺可不一定)
它們的長度變化可能並非我們對64-bit長度加倍的預期,所以說,程式中出現sizeof
的程式碼多看兩眼。而且,除非你明確知道自己在做什麼,應該使用下面的型別代替基本型別:
- int -> NSInteger
- unsigned -> NSUInteger
- float -> CGFloat
- 動畫時間 -> NSTimeInterval
- …
這些都是SDK中定義的型別,而我們大部分時間都在跟SDK的API們打交道,使用它們能將型別轉換的影響降低很多。
再比如說下面的程式碼:
1 2 3 4 |
NSArray *items = @[@1, @2, @3]; for (int i = -1; i < items.count; i++) { NSLog(@"%d", i); } |
結果是,for迴圈一次都沒有進。
陣列的count
是NSUInteger
型別的,-1與其比較時隱式轉換成NSUInteger
,變成了一個很大的數字:
1 2 3 4 |
(lldb) p i (int) $0 = -1 (lldb) p (NSUInteger)i (NSUInteger) $1 = 18446744073709551615 |
這和64-bit到沒啥關係,想要說明的是,這種隱式轉換也需要小心,一定要注意和這個變數相關的所有操作(賦值、比較、轉換)
老式for迴圈可以考慮寫成:
1 |
for (NSUInteger index = 0; index < items.count; index++) {} |
當然,陣列遍歷還是更推薦用for-in
或block
版本的,它們之間的比較可以回顧下這篇文章。
使用新版列舉
和上面的原因差不多,列舉應該使用新版的寫法:
1 2 3 4 5 6 |
typedef NS_ENUM(NSInteger, UIViewAnimationCurve) { UIViewAnimationCurveEaseInOut, UIViewAnimationCurveEaseIn, UIViewAnimationCurveEaseOut, UIViewAnimationCurveLinear }; |
不僅能為列舉值指定型別,而且當賦值賦錯型別時,編譯器還會給出警告,沒理由不用這種寫法。
替代Format字串
適配64-bit時,你是否遇到了下面的噁心寫法:
1 2 |
NSArray *items = @[@1, @2, @3]; NSLog(@"陣列元素個數:%lu", (unsigned long)items.count); |
一般情況下,利用NSNumber
的@
語法糖就可以解決:
1 2 |
NSArray *items = @[@1, @2, @3]; NSLog(@"陣列元素個數:%@", @(items.count)); |
同理,int轉string也可以:
1 2 |
NSInteger i = 10086; NSString *string = @(i).stringValue; |
當然,如需要%.2f
這種Format就不適用了。
64-bit下的BOOL
32-bit下,BOOL被定義為signed char
,@encode(BOOL)的結果是'c'
64-bit下,BOOL被定義為bool
,@encode(BOOL)結果是'B'
更直觀的解釋是:
1 2 3 4 |
(lldb) p/t (signed char)7 (BOOL) $0 = 0b00000111 (YES) (lldb) p/t (bool)7 (bool) $1 = 0b00000001 (YES) |
32-bit版本的BOOL包括了256個值的可能性,還會引起一些坑,像這篇文章所說的。而64-bit下只有0(NO),1(YES)兩種可能,終於給BOOL正了名。
不直接取isa指標
編譯器已經預設禁用了這種使用,isa指標在32位下是Class的地址,但在64位下利用bits mask才能取出來真正的地址,若真需要,使用runtime的object_getClass
和object_setClass
方法。關於64位下isa的講解可以看這篇文章
解決第三方lib依賴和lipo命令
以原始碼形式出現在工程中的第三方lib,只要把target加上arm64
編譯就好了。
噁心的就是直接拖進工程的那些靜態庫(.a)或者framework,就需要重新找支援64-bit的包了。這時候就能看出哪些是已無人維護的lib了,是時候找個替代品了(比如我全網找不到工程中用到的一個音訊庫的64位包,終於在一個哥們的github上找到,哭著給了個star- -)
列印Mach-O檔案支援的架構
如何看一個可執行檔案是不是支援64-bit呢?
使用lipo -info
命令,比如看看UIKit支援的架構:
1 2 3 |
// 當前在Xcode Frameworks目錄 sunnyxx$ lipo -info UIKit.framework/UIKit Architectures in the fat file: UIKit.framework/UIKit are: arm64 armv7s |
想看的更詳細的資訊可以使用lipo -detailed_info
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
sunnyxx$ lipo -detailed_info UIKit.framework/UIKit Fat header in: UIKit.framework/UIKit fat_magic 0xcafebabe nfat_arch 2 architecture arm64 cputype CPU_TYPE_ARM64 cpusubtype CPU_SUBTYPE_ARM64_ALL offset 4096 size 16822272 align 2^12 (4096) architecture armv7s cputype CPU_TYPE_ARM cpusubtype CPU_SUBTYPE_ARM_V7S offset 16826368 size 14499840 align 2^12 (4096) |
當然,還可以使用file
命令:
1 2 3 4 |
sunnyxx$ file UIKit.framework/UIKit UIKit.framework/UIKit: Mach-O universal binary with 2 architectures UIKit.framework/UIKit (for architecture arm64):Mach-O 64-bit dynamically linked shared library UIKit.framework/UIKit (for architecture armv7s):Mach-O dynamically linked shared library arm |
上述命令對Mach-O
檔案適用,靜態庫.a
檔案,framework中的.a
檔案,自己app的可執行檔案都可以列印下看看。
合併多個架構的包
如果,我們有MyLib-32.a
和MyLib-64.a
,可以使用lipo -create
命令合併:
1 |
sunnyxx$ lipo -create MyLib-32.a MyLib-64.a -output MyLib.a |
支援64-bit後程式包會變大麼?
會,支援64-bit後,多了一個arm64
架構,理論上每個架構一套指令,但相比原來會大多少還不好說,我們這裡增加了大概50%,還有聽說會增加一倍的。
一個lib包含了很多的架構,會打到最後的包裡麼?
不會,如果lib中有armv7, armv7s, arm64, i386
架構,而target architecture選擇了armv7s, arm64
,那麼只會從lib中link指定的這兩個架構的二進位制程式碼,其他架構下的程式碼不會link到最終可執行檔案中;反過來,一個lib需要在模擬器環境中正常link,也得包含i386架構的指令。
Checklist
最後列一下官方文件中的注意點:
- 不要將指標強轉成整數
- 程式各處使用統一的資料型別
- 對不同型別的整數做運算時一定要注意
- 需要定長變數時,使用如
int32_t, int64_t
這種定長型別 - 使用malloc時,不要寫死size
- 使用能同時適配兩個架構的格式化字串
- 注意函式和函式指標(型別轉換和可變引數)
- 不要直接訪問Objective-C的指標(isa)
- 使用內建的同步原語(Primitives)
- 不要硬編碼虛存頁大小
- Go Position Independent
References
h/Conceptual/CocoaTouch64BitGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40013501-CH1-SW1
http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
http://www.bignerdranch.com/blog/64-bit-smorgasbord/
http://www.bignerdranch.com/blog/bools-sharp-corners/