前言
在最近的版本中,MSDK要支持bitcode了。于是开始对这个2015WWDC就提出老概念进行了新的学习。很多开发者都知道开启bitcode可以在archive时生成一份中间代码以提交给苹果,让其根据不同的架构生成不同的ipa,以减少安装包体积与应对未来出现新架构不兼容问题。也几乎都碰到过引用第三方库因其不支持bitcode而编译不过。但是Bitcode到底是什么呢?
我们知道编译器前端可以把不同的高级语言转换为中间代码(IR),再由编译器后端将中间代码转换成指定平台的目标代码,再通过链接器把目标代码和依赖的库文件link到一起即可变成可执行文件。因此理论上只需要中间代码即可生成任一平台的目标代码(只要编译器后端支持),因为中间代码已经包含了源程序所所要表达的全部意思。
再看下LLVM官方文档的描述:
1 | The LLVM code representation is designed to be used in three different forms: as an in-memory compiler IR, as an on-disk bitcode representation (suitable for fast loading by a Just-In-Time compiler), and as a human readable assembly language representation.This allows LLVM to provide a powerful intermediate representation for efficient compiler transformations and analysis, while providing a natural means to debug and visualize the transformations. The three different forms of LLVM are all equivalent. |
重点是三种代码表现形式:编译过程中的中间代码IR;编译出的bitcode;可读的汇编代码。其实都是对源代码的一种描述,只是面向了不同的对象时的表现形态。由此可知bitcode其实只是IR的另一种表现形式。
bitcode可以做什么
1.首先我们写一个小demo:
1 |
|
2.把源文件转换为LLVM的中间表示bitcode文件:
1 | clang -emit-llvm -c hello.c -o hello.bc |
可以看到hello.bc文件格式如下:
1 | file hello.bc |
3.然后把bitcode转换成目标文件,输出为Mach-O文件:
1 | clang -c hello.bc -o hello.bc.o |
4.直接把源代码编译成目标文件并和由bitcode生成目标文件进行对比:
1 | clang -c hello.c -o hello.o |
可以发现通过bitcode获取和直接编译出来的目标代码是一模一样的。因此只需要得到bicode文件就可以编译出一样的目标文件。
Bitcode是什么:
通过LLVM官方对bitcode的描述,可以知道bitcode是一种以位为单位存取的二进制文件,它可以存在于包装结构中,如上一节中看到LLVM bitcode, wrapper x86_64
,也可以存在于Object
文件中,例如Mach-O
文件等。对于Mach-O
文件并且必定存在于名为__LLVM
或者__bitcode
的section中,因此我们可以根据这两个字段来判断生成的Mach-O文件是否包含bitcode.
Bitcode的优化
1.首部
Bitcode利用首部简单明了的描述了Bitcode文件的信息。通过该首部编译器可以快速确定该如何编当前的Bitcode文件。
采用前4个字节是固定的magic number
来标识是,目前一直是0x0B17C0DE
。当然bitcode文件格式仍然还在变化,这并不能作为唯一识别bitcode文件的依据。
然后4个字节描述了CPU架构,实际是为了表示Bitcode文件的字节序,以便编译器可以正确读取。
之后描述了Load command
,文件类型信息等。
2.整形长度
bitcode为了减少体积,充分利用了按位存取的特定,根据数据类型更精确的分配数据长度,例如boolean
只需要一个bit
即可。而且利用变长整形来encode数据来减少表示值很小数时的内存浪费,它按4个bit这样为一个feild来表示数据,最高位表示是否连续(即和后面的数据是否是组合的),后面的3个bit表示0-7的数,对于连续的数字,后面的接的feiled通过移位<<3
后和前面的feild的数据组合到一起。
例如数字27在bitcode中表示为1011 0011
,前一个feild值为3,由于最高位1表示连续,则后一个feild的011
需要左移3个bit即24,而后一个feild最高位为0表示不再连续。则数据为:24+3 = 27
.
咋一看浪费了两个bit来表示是否连续,但是首先它只占用了一个字节,,而且对于比1111 0111
略大一点的数时,只需要在连续一个feild即4个bit即可,而整形恰好又是最基本的数据。可见这里的可以减少非常多的内存空间。
3.6个bit的字符
在bitcode中,只用6个bit来表示字符。因为它只需要a-z
,A-Z
,0-9
和.-
共64个字符。所以所谓优化,最重要的就是把不需要的东西都去掉。。。
上面我们知道bitcode文件是一种LLVM bitcode文件,实际是一些字节因此无法阅读,不过llvm-dis是一个可以可以把bitcode文件转换为LLVM表示的反汇编的可视化的汇编语言(注意与Clang下的表示不一致):
1 | llvm-dis < hello.bc |
同样的-S选项可以把源文件转换为LLVM表示的汇编语言(注意与Clang下的表示不一致):
1 | clang -emit-llvm -S hello.c -o hello.ll |
打开hello.ll可以看到如下,我加了一些comment来简单的描述:
1 | <---描述bitcode文件的基本信息,数据对其---> |
大概可以了解到bitcode文件记录了源文件的一些基本信息如ModuleID(参考XXXX),文件名,ABI。然后包含源代码的解析。最后保留了构建bitcode文件的编译器的版本。
Bitcode的结构
我们分别编译一个不带bitcode的Object文件和一个带有bitcode的object文件,然后进行对比:
1 | otool -l main_bitcode.o >> main_bitcode.o.txt |
可以看到开启bitcode后几乎每个section都会变大,并且多了名为__bitcode
的section和名为__cmdline
的section。
通过segedit
可以提取出指定section.
1 | segedit -extract __LLVM __bitcode main_bitcode.o.bc \ |
对提取出的bitcode和直接编译出的bitcode文件进行MD5校验:
1 | md5 main.bc main_bitcode.o.bc |
并且查看文件大小可以看到:
1 | ls -al main.o main.bc main_bitcode.o.bc |
可以看到导出的bitcode和直接编出的bitcode并不完全一致,所以这个section并没有完全包含bitcode的信息。
__cmdline
section存储了一些用于bitcode重建object文件是的clang编译选项,只有会影响代码生成和没有存在bitcode section的属性才会存在这里。
链接时,链接器把Object文件链接到一起生成Mach-O文件,把不同的object文件中的__bitcode
section中的数据取出来并放到不同的文件中,这里文件会连同__cmdline
中的编译选项整合为xar-archive,这是macOS上的一种归档格式,类似我们archive出来的文件。产生的archive文件会保存在__LLVM
segment中的__bundle
section中。
直接查看含有bitcode的可执行文件中的__LLVM
segment下的__bundle
section如下, otool -v可以打印出可视化的符号:
1 | otool -v -s __LLVM __bundle a.out |
可以看到,里面__bundule
section就是一个xml配置文件,包含了link选项配置,数据长度,和通过__cmdline
重建object文件的clang选项等等。因此通过它就可以创建可执行文件。
我们尝试从中提取出bitcode文件:
1 | //1.提取出bitcode相关的bundle |
我们对比bundle描述头文件和通过otool打印出的bundle描述文件一模一样。
1 | //1.加压出bitcode文件,自动生成在当前文件夹下名为1 |
比较从__bitcode
section和 链接后从__bundle
中提取出的bitcode文件发现是完全一致的,说明链接时只是单纯的对bitcode进行拷贝。
小结
经过本章的分析,对Bitcode文件是什么,所做的优化,文件结构,以及怎么使用有了比较深的认识。其中可以学习到的是Bitcode优化的部分,其中的思想其实也可以应用再我们的业务中。同时再次遇到Bitcode时也不在会是一脸懵逼。同时还更熟悉了一些系统工具的使用,可以有效的帮助我们在遇到一些难解问题时进行分析。
Bitcode使用
通过Bitcode文件生成Mach-O文件
通过Clang的-fembed-bitcode
描述是生成的Object文件内嵌bitcode:
1 | clang -fembed-bitcode -c main.cc -o main.o |
利用objdump或者otool工具可以查看Object文件,根据官方文档说明,内嵌bitcode文件后,会出现__LLVM字段,因此可以依据此来判断bitcode是否生效:
1 | otool -l main.o | grep __LLVM |
或是:
1 | objdump -all-headers mian_bitcode.out | grep __LLVM |
需要注意的一点是,如果单纯在buildSetting中Enable Bitcode
编出来的products是不带bitcode。例如,现在需要编译一个动态库,设置Enable Bitcode
为YES。 然后选择模拟器,编译一个x86_64
版本的动态库。通过查找__LLVM
section发现并没有bitcode。
我们查看一下编译输出:
1 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x objective-c -target x86_64-apple-ios9.0-simulator -fmessage-length=0 -fdiagnostics-show-note-include-stack -fmacro-backtrace-limit=0 -std=gnu11 -fobjc-arc -fobjc-weak -fmodules -gmodules -fmodules-cache-path=/Users/joey.cao/Desktop/DJINetwork/djinetworkrtkhelper/DJINetworkRTKHelper/DerivedData/ModuleCache.noindex -fmodules-prune-interval=86400 -fmodules-prune-after=345600 |
为了好看,把中间的option选项省略了,可以看到其实就是利用clang,然后把build setting里的编译配置作为option来编译出了一个Object文件到build文件夹下面。 但是build option中并没有bitcode,所以编出来的产物自然不带bitcode。
脚本构建带有Bitcode的Mach-O文件
因此我们应该利用脚本主动调用-fembed-bitcode
来生成带有bitcode的产物。正好,我们编出的动态库还需要同时支持多个架构,如果完全依赖xcode,我们得真机下编一次,模拟器编一次,再手动合并成fat file
。而这都可以通过脚本来处理:
1 | xcodebuild -project "${SRCROOT}/${PROJECT_NAME}.xcodeproj" -configuration "${CONFIGURATION}" -scheme "${PROJECT_NAME}" -sdk iphoneos VALID_ARCHS="x86_64 arm64 arm64e" -derivedDataPath "${SRCROOT}" clean |
我们利用xcodebuild传入OTHER_CFLAGS
,OTHER_CFLAGS
的选项会直接传递给编译器,因此clang就具有了-fembed-bitcode
选项:
1 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x objective-c -arch x86_64 -fmessage-length=0 |
通过刚才的测试可以看到如果没有-fembed-bitcode
编译的产物是不包含__LLVM
和__bitcode
字段的,对编出来的fat file
进行检查如下,可以看到已经包含了bitcode文件:
1 | otool -arch arm64 -l /Users/joey.cao/Desktop/DJINetwork/djinetworkrtkhelper/DJINetworkRTKHelper/Output/DJINetworkRTKHelper.framework/DJINetworkRTKHelper | grep __LLVM |
Bitcode应用上架
使用bitcode嘛最终目的还是为了应用的上架,但是比较坑爹是所有依赖的库文件必须支持bitcode。否则构建的mach-O文件就不能支持bitcode。假设我们目前所有依赖的库文件都已经支持bitcode了,我们使用一个MSDK预上架的工程archive一个支持bitcode的和一个不支持bitcode。可以看到以下区别
1.惊人发现开启bitcode之前归档出的文件只有118.5MB,而开启bitcode之后直接增加到254MB。这下是不是要超过Apple的文件大小限制了啊。根据前面的测试我们知道bitcode放在__LLVM
section,而苹果二进制文件大小限制只是针对__Text
段,即代码段的,因此bitcode文件并不会影响苹果的限制。
2.当我们需要上传App时,若果开启了bitcode的App会提示选择是否上传bitcode内容。如果勾选了bitcode,则同时会上传bitcode到苹果服务器,并且用户下载的ipa是苹果针对bitcode再次编译出来的。而如果不勾选,则用户下载的是我们上传的ipa,并且会剔除bitcode。 如果选择的Development发布方式,则会出现选择Rebuild from bitcode
,此时如果勾选则会发现会利用bitcode-build-tool
来构建ipa文件,而不是直接利用归档出的ipa。这样就跟苹果在服务器归档后发布给用户的ipa一致了:
1 | /Applications/Xcode.app/Contents/Developer/usr/bin/bitcode-build-tool -v -t /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin --sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk -o /var/folders/64/2_nlwt_s6kq250l7_t5mfdzwrh9_pg/T/ipatool20191119-81739-om7ou4/thinned-out/arm64/Payload/DJI\ MSDK\ Preview.app/DJI\ MSDK\ Preview --generate-dsym /var/folders/64/2_nlwt_s6kq250l7_t5mfdzwrh9_pg/T/ipatool20191119-81739-om7ou4/thinned-out/arm64/Payload/DJI\ MSDK\ Preview.app/DJI\ MSDK\ Preview.dSYM --strip-swift-symbols /var/folders/64/2_nlwt_s6kq250l7_t5mfdzwrh9_pg/T/ipatool20191119-81739-om7ou4/thinned-in/arm64/Payload/DJI\ MSDK\ Preview.app/DJI\ MSDK\ Preview |
3.开启bitcode的归档文件下面多了个BCSymbolMaps
的文件夹。这也可以作为一个判定开启bitcode成功的标志。BCSymbolMaps
是一个类似dSYM的文件,可以辅助定位crash,因为我们知道苹果会根据bitcode再次构建ipa,那么原有的dSYM已经不能用了。因此当出现crash,必须通过App Store Connect下载新的dSYM来进行定位。
4.Cocoapods也是支持bitcode的,可以通过配置podfile快速为所有target设置:
1 | post_install do |installer| |