前言
做iOS開發的朋友們都知道,目前最新的Xcode7,新建項目默認就打開了bitcode設置.而且大部分開發者都被這個突如其來的bitcode功能給坑過導致項目編譯失敗,而這些因為bitcode而編譯失敗的的項目都有一個共同點,就是鏈接了第三方二進制的庫或者框架,而這些框架或者庫恰好沒有包含bitcode的東西(暫且稱為東西),從而導致項目編譯不成功.所以每當遇到這個情況時候大部分人都是直接設置Xcode關閉bitcode功能,全部不生成bitcode.也不去深究這一開關背后隱藏的原理.中槍的請點個贊.
LLVM是目前蘋果采用的編譯器工具鏈,Bitcode是LLVM編譯器的中間代碼的一種編碼,LLVM的前端可以理解為C/C++/OC/Swift等編程語言,LLVM的后端可以理解為各個芯片平台上的匯編指令或者可執行機器指令數據,那么,BitCode就是位於這兩者直接的中間碼. LLVM的編譯工作原理是前端負責把項目程序源代碼翻譯成Bitcode中間碼,然后再根據不同目標機器芯片平台轉換為相應的匯編指令以及翻譯為機器碼.這樣設計就可以讓LLVM成為了一個編譯器架構,可以輕而易舉的在LLVM架構之上發明新的語言(前端),以及在LLVM架構下面支持新的CPU(后端)指令輸出,雖然Bitcode僅僅只是一個中間碼不能在任何平台上運行,但是它可以轉化為任何被支持的CPU架構,包括現在還沒被發明的CPU架構,也就是說現在打開Bitcode功能提交一個App到應用商店,以后如果蘋果新出了一款手機並CPU也是全新設計的,在蘋果后台服務器一樣可以從這個App的Bitcode開始編譯轉化為新CPU上的可執行程序,可供新手機用戶下載運行這個App.
歷史回顧
在iPhone出來之前,蘋果主要的編譯器技術是用經過稍微改進的GCC工具鏈來把Objective-C語言編寫的代碼編譯出所指定的機器處理器上原生的可執行程序.編譯器產生的可執行程序叫做"Fat Binaries"--類似於Windows下PE格式的exe和Linux下的ELF格式的二進制,不同的是,一個"Fat Binary"可以包含同一個程序的很多版本,所以同一個可執行文件可以在不同的處理器上運行.主要就是這個技術讓蘋果的硬件很容易的從PowerPC遷移到PowerPC64的處理器,以及后來再遷移到Intel和Intel64處理器.這個方案帶來的負面影響就是同一個文件中存了多份可執行代碼,除了當前機器可執行的那一份之外其他都是無用的,白占空間. 這個在市場上被稱為"Universal Binary",在蘋果從PowerPC遷移到Intel處理器的事情開始存在的(一個二進制文件既包含一份PowerPC版本和一份Intel版本).慢慢的后來又支持同時包含Intel 32bit和Intel 64bit. 在一個Fat binary中,又操作系統運行時根據處理器類型動態選擇正確的二進制版本來運行,但是應用程序要支持不同平台的處理器的話,應用程序本身要多占用一些空間.當然也有一些瘦身的工具,比如lipo,可以用來移除fat binary中那些當前機器中不被支持的或者多余的可執行代碼達到瘦身目的,lipo不會改變程序執行邏輯,僅僅只是文件的大小瘦身.
編譯器現狀
隨着移動設備移動互聯網的深入發展,現在移動設備中的程序大小變得越來越重要了,主要是因為移動設備中不會有電腦上那么大的一個硬盤驅動器.還有就是蘋果早就從原始的ARM處理器遷移到自家設計的A4,A5,A5X,A6,A7,A8,A8X,A9,A9X以及后續的A10處理器,他們的指令集已經發生了改變和原始ARM設計的有所區別,所有的這些變化都被iOS操作系統底層以及Xcode/LLVM編譯工具向上層程序員一定程度的透明了,編譯出來的程序會包含很多執行代碼版本.當面對這個問題后,蘋果投入大量成本遷移到LLVM編譯器架構並使用bitcode的必要性越來越大.從最開始的把OPENGL編譯為特定的GPU指令到把Clang編譯器(LLCM的C/OC編譯前端)支持Objective-C的改進並作為Xcode的默認編譯器.
LLVM提供了一個虛擬指令集機制,它可以翻譯出指定的所支持的處理器架構的執行代碼(機器碼).這個就使得為iOS應用程序的編譯開發一個完全基於LLVM架構的工具鏈成為可能.而LLVM的這個虛擬的通用的指令集可以用很多種表示格式:
- 叫做IR的文本表示的匯編格式(像匯編語言);
- 轉換為二進制數據表示的格式(像目標代碼),這個二進制格式就是我們所說的bitcode.
Bitcode和傳統的可執行指令集不同,他維護的是函數功能的類型和簽名,比如,傳統可執行指令集中,一系列(<=8)的布爾值可以壓縮存儲到單個字節中,但是在bitcode中他們是各自獨自表示的.此外,邏輯運算操作(比如寄存器清零操作)也由他們對應的邏輯表示方法($R=0
);當這些BitCode要轉換為特定機器平台的指令集時,他可以用經過針對特定機器平台優化過的匯編指令來代替:xor eax, eax
.(這個匯編指令同樣是寄存器<eax>清零操作).
然而bitcode他也不是完全獨立於處理器平台和調用約定的.寄存器的大小在指令集中是一個相當重要的特性,眾所周知,64bit寄存器可以比32bit寄存器存儲更多的數據,生成64bit平台的bitcode和32bit平台的bitcode是明顯不同的,還有,調用約定可以根據函數定義或者函數調用來定義,這些可以確定函數的參數傳遞是傳寄存器值呢還是壓棧. 一些編程語言還有一些像sizeof(long)這樣的預處理指令,這些將在bitcode生成之前前被翻譯.一般情況下,對於支持fastcc
(fast calling convention)調用的64bit平台會生成與其一致的bitcode代碼.
蘋果的要求
到此,讓我們思考一下,為什么蘋果默認要求watchOS和tvOS的App要上傳bitcode? 因為把bitcode上傳到他自己的中心服務器后,他可以為目標安裝App的設備進行優化二進制,減小安裝包的下載大小,當然iOS開發者也可以上傳多個版本而不是打包到單個包里,但是這樣會占用更多的存儲空間. 最重要的是允許蘋果可以在后台服務器對應用程序進行簽名,而不用導出任何密鑰到終端開發者那.
上傳到服務器的bitcode給蘋果帶來更好處是: 以后新設計了新指令集的新CPU,可以繼續從這份bitcode開始編譯出新CPU上執行的可執行文件,以供用戶下載安裝.
但是bitcode給開發者帶來的不便之處就是: 沒用bitcode之前,當應用程序奔潰后,開發者可以根據獲取的的奔潰日志再配上上傳到蘋果服務器的二進制文件的調試符號表信息可以還原程序運行過程到奔潰時后調用棧信息,對問題進行定位排查.但是用了bitcode之后,用戶安裝的二進制不是開發者這邊生成的,而是蘋果服務器經過優化后生成的,其對應的調試符號信息丟失了,也就無法進行前面說的還原奔潰現場找原因了.
目前,watchOS和tvOS應用發布必須上傳帶bitcode版本的包.iOS應用發布對bitcode的要求是可選的,用戶可以在Xcode的項目設置中關閉. 相當於在編譯的時候加一個標記:embed-bitcode-marker(調試構建) embed-bitcode(打包/真機構建).這個在clang編譯器的參數是-fembed-bitcode,swift編譯器的參數是-embed-bitcode.
實踐出真知
我們還是應該實際弄兩個測試代碼進行實踐和檢驗一下比較好.做兩次測試,第一次准備兩個C語言源代碼繼續測試;第二次把其中一個轉變為匯編語言源代碼后再一個C代碼和一個匯編代碼一起重復之前的測試步驟進行對比校驗差異.
- 1 . 如下兩個全部是Objective-C代碼:
test.m :
#import <Foundation/Foundation.h> void greeting(void) { NSLog(@"hello world!"); }
demo.m :
#import <Foundation/Foundation.h> void demo(void) { NSLog(@"demo func"); }
用Clang編譯成 ARM64 格式且帶bitcode的目標文件test.o demo.o:
wuqiong:~ apple$ xcrun -sdk iphoneos clang -arch arm64 -fembed-bitcode -c test.m demo.m
然后把兩個目標文件打包為一個靜態庫文件:
wuqiong:~ apple$ xcrun -sdk iphoneos ar -r libTest.a test.o demo.o ar: creating archive libTest.a
用Shell命令otool查看目標文件中是否包含bitcode段:
wuqiong:~ apple$ otool -l test.o |grep bitcode sectname __bitcode sectname __bitcode
如果看到輸出了2行sectname __bitcode
,就是說明這靜態庫中的兩個目標文件包含了bitcode.
- 2.下面把其中一個demo.m換成匯編語言再參與編譯:
用下面的命令把demo.m的C代碼轉換為ARM64匯編語言格式demo.s:
wuqiong:~ apple$ xcrun -sdk iphoneos clang -arch arm64 -S demo.m wuqiong:~ apple$ cat demo.s .section __TEXT,__text,regular,pure_instructions .ios_version_min 9, 2 .globl _demo .align 2 _demo: ; @demo .cfi_startproc ; BB#0: stp x29, x30, [sp, #-16]! mov x29, sp Ltmp0: .cfi_def_cfa w29, 16 Ltmp1: .cfi_offset w30, -8 Ltmp2: .cfi_offset w29, -16 adrp x0, L__unnamed_cfstring_@PAGE add x0, x0, L__unnamed_cfstring_@PAGEOFF bl _NSLog ldp x29, x30, [sp], #16 ret .cfi_endproc .section __TEXT,__cstring,cstring_literals L_.str: ; @.str .asciz "demo func" .section __DATA,__cfstring .align 4 ; @_unnamed_cfstring_ L__unnamed_cfstring_: .quad ___CFConstantStringClassReference .long 1992 ; 0x7c8 .space 4 .quad L_.str .quad 9 ; 0x9 .section __DATA,__objc_imageinfo,regular,no_dead_strip L_OBJC_IMAGE_INFO: .long 0 .long 0 .subsections_via_symbol
然后刪除demo.m
這個C源代碼,僅留下test.m
和demo.s
:
wuqiong:~ apple$ rm demo.m
現在,我們來把test.m
這個C源代碼和dmeo.s
這個匯編源代碼來一起帶着-fembed-bitcode
參數來生成目標代碼並打包為一個靜態庫:
wuqiong:~ apple$ xcrun -sdk iphoneos clang -arch arm64 -fembed-bitcode -c test.m demo.s wuqiong:~ apple$ xcrun -sdk iphoneos ar -r libTest.a test.o demo.o
然后我們再運行otool工具來檢查這個新的靜態庫中包含的2個目標文件是否都帶有bitcode段:
wuqiong:~ apple$ ar -t libTest.a __.SYMDEF SORTED test.o demo.o wuqiong:~ apple$ otool -l libTest.a | grep bitcode sectname __bitcode
很意外,這一次,只有一行sectname __bitcode
輸出,這就說明這兩個目標文件,有一個不帶有bitcode段,哪怕我們在編譯的時候指定了參數-fembed-bitcode
也沒有用.至於具體是哪一個不帶bitcode段,我們肯定知道就是那個從ARM64匯編語言編譯過來的目標文件不帶.
那么就得出一個結論,bitcode的生成,是由匯編語言以上的上層語言編譯而來,和最前面所說的那樣,他是上層語言與匯編語言(機器語言)之間的一個中間碼.
目前我們日常的iOS應用開發中,一般不會需要用到匯編層面去優化的代碼.所以我們主要關注第三方(開源)C代碼,尤其是音視頻編碼解碼這些計算密集型項目代碼,關鍵計算的代碼針對特定平台都有對應平台的匯編版本實現,當然也有C的實現,但是默認編譯一般都是用的匯編版本,這樣就會導致我們在編譯這個開源代碼的時候哪怕你帶了-fembed-bitcode
參數也僅僅只是讓項目中的部分C代碼的目標文件帶了bitcode段,而那小數的匯編代碼的目標文件一樣不帶bitcode段,這樣編譯出這個庫交給上層開發者使用的時候,就會出現在打包上傳或者真機調試的時候因為Xcode默認開了bitcode功能而鏈接失敗,導致不能真機調試或者不能上傳應用到AppStore.
此文轉載自:http://www.jianshu.com/p/f42a33f5eb61