編譯流程html
Xcode常見編譯報錯分析前端
應用場景ios
拖更好久了,今天水文一篇。簡單介紹下iOS底層編譯的相關知識,幫助咱們充分理解了iOS編譯的過程,相信會對咱們後續的開發有必定幫助。面試
首先看一下iOS代碼是如何從源碼變成可執行文件的,有助於咱們瞭解程序從編譯到運行的全流程objective-c
編譯器是將編程語言轉換爲目標語言的程序,大多數編譯器由兩部分組成:前端和後端。編程
先後端依賴統一格式的中間代碼(IR),使得先後端能夠獨立的變化。新增一門語言只須要修改前端,而新增一個CPU架構只須要修改後端便可。swift
Objective C/C/C++使用的編譯器前端是clang,swift是swift,後端都是LLVM。segmentfault
LLVM是一個模塊化和可重用的編譯器和工具鏈技術的集合,Clang 是 LLVM 的子項目,是 C,C++ 和 Objective-C 編譯器,目的是提供驚人的快速編譯,比 GCC 快3倍,後端
LLVM 還能夠提供一種代碼編寫良好的中間表示 IR,這意味着它能夠做爲多種語言的後端,這樣就可以提供語言無關的優化同時還可以方便的針對多種 CPU 的代碼生成。緩存
Objective-C的編譯器前端是Clang,誕生之初是爲了替代GCC,提供更快的編譯速度。咱們能夠經過下面這張圖來了解Clang編譯的大體流程:
下面咱們經過clang命令來具體分析下源碼編譯的流程:
首先在命令行裏輸入
clang -ccc-print-phases main.m
能夠看到源文件編譯須要的幾個不一樣的階段
➜ clang -ccc-print-phases main.m 0: input, "main.m", objective-c 1: preprocessor, {0}, objective-c-cpp-output //預編譯 2: compiler, {1}, ir //編譯成中間代碼ir 3: backend, {2}, assembler //生成彙編 4: assembler, {3}, object //生成目標文件.O 5: linker, {4}, image //連接成可執行文件 6: bind-arch, "x86_64", {5}, image
接下來咱們新建一個main.m並詳細來看下每一個步驟分別作了什麼
main.m #include <stdio.h> int main() { printf("hello world\n"); return 0; }
咱們用下面的命令來查看clang預處理的結果:
clang -E main.m
注:若是main.m中用到了UIKit等類,能夠在命令後添加-sysroot參數,記得將sdk換成你本機的版本,後續命令解決方法相同。以下所示:
clang -E main.m -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk
能夠看到預處理後的文件行數有不少,在最後能夠找到main函數
# 13 "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk/System/Library/Frameworks/UIKit.framework/Headers/ShareSheet.h" 2 3 # 17 "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk/System/Library/Frameworks/UIKit.framework/Headers/UIKit.h" 2 3 # 10 "main.m" 2 # 1 "./AppDelegate.h" 1 # 11 "./AppDelegate.h" @interface AppDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic) UIWindow *window; @end # 11 "main.m" 2 int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, ((void *)0), NSStringFromClass([AppDelegate class])); } }
預處理會替進行頭文件引入(遞歸操做),宏替換#define,註釋處理,條件編譯(#ifdef),#pargma處理等操做。好比#include "stdio.h"就是告訴預處理器將這一行替換成頭文件stdio.h中的內容,這個過程是遞歸的:由於stdio.h也有可能包含其頭文件。
預處理完成後就會進行詞法分析,這裏會把代碼切成一個個 Token,好比大小括號,等於號還有字符串等。
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
語法分析會校驗語法的正確性,而後將全部的節點組成抽象語法樹AST。有了抽象語法樹,clang就能夠對這個樹進行分析,找出代碼中的錯誤。好比類型不匹配,亦或Objective C中向target發送了一個未實現的消息。
業內對Clang自定義插件或者開發靜態檢測插件都是基於AST語法樹來分析。相關知識後續會學到。AST是開發者編寫clang插件主要交互的數據結構,clang也提供不少API去讀取AST。更多細節: Introduction to the Clang AST。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
在輸出裏能夠看到相關的AST結果,以下圖:
CodeGen 會負責將語法樹自頂向下遍歷逐步翻譯成 LLVM IR,IR 是編譯過程的前端的輸出,也是後端的輸入。
Objective C代碼也在這一步會進行runtime的橋接:property合成,ARC處理等。
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
查看main.ll的內容以下:
... ; Function Attrs: noinline optnone ssp uwtable define i32 @main(i32, i8**) #0 { %3 = alloca i32, align 4 %4 = alloca i32, align 4 %5 = alloca i8**, align 8 store i32 0, i32* %3, align 4 store i32 %0, i32* %4, align 4 store i8** %1, i8*** %5, align 8 %6 = call i8* @llvm.objc.autoreleasePoolPush() #1 %7 = load i32, i32* %4, align 4 %8 = load i8**, i8*** %5, align 8 %9 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8 %10 = bitcast %struct._class_t* %9 to i8* %11 = call i8* @objc_opt_class(i8* %10) %12 = call %0* @NSStringFromClass(i8* %11) %13 = bitcast %0* %12 to i8* %14 = notail call i8* @llvm.objc.retainAutoreleasedReturnValue(i8* %13) #1 %15 = bitcast i8* %14 to %0* %16 = call i32 @UIApplicationMain(i32 %7, i8** %8, %0* null, %0* %15) store i32 %16, i32* %3, align 4 %17 = bitcast %0* %15 to i8* call void @llvm.objc.release(i8* %17) #1, !clang.imprecise_release !10 call void @llvm.objc.autoreleasePoolPop(i8* %6) %18 = load i32, i32* %3, align 4 ret i32 %18 } ; Function Attrs: nounwind ...
若是在項目配置中開啓了 bitcode, 蘋果還會作進一步的優化,有新的後端架構仍是能夠用這份優化過的 bitcode 去生成。
clang -emit-llvm -c main.m -o main.bc
clang -S -fobjc-arc main.m -o main.s
彙編器以彙編代碼做爲輸入,將彙編代碼轉換爲機器代碼,最後輸出目標文件(object file)
clang -fmodules -c main.m -o main.o
接下來咱們用nm命令,查看下main.o中的符號
➜ BuildTest nm -nm main.o (undefined) external _printf 0000000000000000 (__TEXT,__text) external _main
這裏能夠看到_printf是一個是undefined external的。undefined表示在當前文件暫時找不到符號_printf,而external表示這個符號是外部能夠訪問的,對應表示文件私有的符號是non-external。
連接器能夠把編譯產生的.o文件和(dylib,a,tbd)文件,生成一個mach-o文件
clang main.o -o main
接着在命令行執行./main,能夠看到輸出告終果:hello world。
最後咱們用nm命令來分析下可執行文件的符號表:
➜ BuildTest nm -nm main (undefined) external _printf (from libSystem) (undefined) external dyld_stub_binder (from libSystem) 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header 0000000100000f60 (__TEXT,__text) external _main 0000000100002008 (__DATA,__data) non-external __dyld_private
能夠看到_printf仍然是undefined,可是後面多了一些信息:from libSystem,表示這個符號來自於libSystem,會在運行時動態綁定。
以上就是Clang編譯源文件的完整流程了。
若是你想在 Xcode 中查看,能夠經過 Show the report navigator 裏對應 target 的 build 中查看每一個 .m 文件的 clang 編譯信息,以下圖:
隨便找一個.m文件編譯信息,能夠看到Xcode會首先對任務進行描述:
CompileC /Users/chenaibin/Library/Developer/Xcode/DerivedData/PodIntegrationDemo-achbuytjuwbatqbzvlwflifarxwa/Build/Intermediates.noindex/Pods.build/Debug-iphonesimulator/podLibB.build/Objects-normal/x86_64/podClsB.o /Users/chenaibin/Work/DiDi/iOSDemo/BuildErrorDemo/podLibB/Classes/podClsB.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler (in target 'podLibB' from project 'Pods')
接下來對會更新工做路徑,同時設置 PATH
cd /Users/chenaibin/Work/DiDi/iOSDemo/BuildErrorDemo/PodIntegrationDemo/Pods export LANG=en_US.US-ASCII export PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
接下來就是實際的編譯命令
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x objective-c -target x86_64-apple-ios9.0-simulator -fmessage-length=0 -fobjc-arc… -Wno-missing-field-initializers ... -DDEBUG=1 ... -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk -iquote ... -I... -F...-c /.../podClsB.m -o /.../podClsB.o
clang 用到的命令參數以下:
-x 編譯語言好比objective-c -arch 編譯的架構,好比arm64 -f 以-f開頭的。 -W 以-W開頭的,能夠經過這些定製編譯警告 -D 以-D開頭的,指的是預編譯宏,經過這些宏能夠實現條件編譯 -iPhoneSimulator13.0.sdk 編譯採用的iOS SDK版本 -I 把編譯信息寫入指定的輔助文件 -F 須要的Framework -c 標識符指明須要運行預處理器,語法分析,類型檢查,LLVM生成優化以及彙編代碼生成.o文件 -o 編譯結果
第一個常見的編譯報錯緣由就是duplicate symbols,以下圖就是由於咱們連接後的可執行文件存在了重複的類致使的。
注:因爲咱們工程是由CocoaPods構建的,在xcconfig中OTHER_LINK_FLAG都會被默認設置成$(inherited) -ObjC ......,這會致使工程配置裏Other Linker Flags會帶上 -ObjC標記,若是咱們手動刪除了-ObjC,就會發如今編譯時不會有duplicate symbols的錯誤了。可是運行的時候可能會出現unrecognized selector sent to class XXX的錯誤,這是因爲靜態庫中的分類並沒被連接器連接進可執行文件中。
-ObjC會把靜態庫中全部的類和分類都連接進可執行文件,因此會出現duplicate symbols的錯誤。下面是官方描述:
This flag causes the linker to load every object file in the library that defines an Objective-C class or category. While this option will typically result in a larger executable (due to additional object code loaded into the application), it will allow the successful creation of effective Objective-C static libraries that contain categories on existing classes.
第二個常見報錯是在某個架構下找不到相關符號,這是由於引用的某個靜態庫並無包含當前工程制式下的架構類型,解決方案是將靜態庫.a文件合併x86_64/arm64等架構爲fat file,再集成到工程裏使用。
報錯緣由以下圖:
提示:遇到這種狀況時,有時候屢次pod update也不能解決報錯緣由。這是由於你本地緩存了有問題的靜態庫文件,可在如下目錄下找到相關類庫並刪除,再執行pod install下載fix後的靜態庫文件。CocoaPods官方緩存目錄:~/Library/Caches/CocoaPods/Pods
這個錯誤還有另一種狀況,當同一個pod在多個不一樣的端集成時可能會遇到。報錯信息大體以下:
問題緣由:在ProjectA中集成了podA和podB,podA使用了#if __has_include("podB中的cls.h")集成了podB中的類;當切換到ProjectB時,只會依賴podA一個庫,這個時候編譯就會上圖中的錯誤。
解決方案:在ProjectB中將podA以源碼從新編譯一遍便可。
在平時開發中,咱們常常會遇到頭文件裏有__attribute__的用法,它是一個高級的的編譯器指令,它容許開發者指定更更多的編譯檢查和一些高級的編譯期優化。
__attribute__
語法格式爲:__attribute__ ((attribute-list)) 放在聲明分號「;」前面。
好比,在三方庫中最多見的,聲明一個屬性或者方法在當前版本棄用了
@property (strong,nonatomic)CLASSNAME * property __deprecated;
下面是 iOS開發中常見的幾個 __attribute__
用法:
//棄用API,用做API更新 #define __deprecated __attribute__((deprecated)) //帶描述信息的棄用 #define __deprecated_msg(_msg) __attribute__((deprecated(_msg))) //遇到__unavailable的變量/方法,編譯器直接拋出Error #define __unavailable __attribute__((unavailable)) //告訴編譯器,即便這個變量/方法 沒被使用,也不要拋出警告 #define __unused __attribute__((unused)) //和__unused相反 #define __used __attribute__((used)) //若是不使用方法的返回值,進行警告 #define __result_use_check __attribute__((__warn_unused_result__)) //OC方法在Swift中不可用 #define __swift_unavailable(_msg) __attribute__((__availability__(swift, unavailable, message=_msg)))
當咱們在XCode中屏蔽部分Warning信息時,可使用下面的內容來解決。經過clang diagnostic push/pop來控制代碼塊的編譯選項。
#pragma clang diagnostic push #pragma clang diagnostic ignored "-Wundeclared-selector" ///代碼 #pragma clang diagnostic pop
預處理可讓咱們讓咱們自定義編譯器變量,實現條件編譯。 好比咱們經常使用的DEBUG宏:
#ifdef DEBUG //... #else //... #endif
咱們能夠在XCode的Target中選中Build Setting選項,搜索proprecess,便可看到定義好的預處理宏。
目前iOS基本都是用CocoaPods來管理工程,咱們也能夠在每一個Pod的podspec文件中配置預編譯宏,CocoaPods會在構建工程時將這些信息寫到Pod的xcconfig文件裏。
# Pod.podspec示例 s.subspec 'YourSubSpec' do | ss | ss.source_files = 'Pod/Classes/**/*' ss.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) YOUR_CUSTOM_DEFINE=1' } end
注意:podA定義的GCC_PREPROCESSOR_DEFINITIONS內容在podB中是不生效的!!!
若是想解決這個問題,推薦podB中單獨定義一個subspec來配置預編譯宏的值,在外層工程裏經過區分是否引入podB的subspec來實現該預編譯宏值的控制。
上面介紹到語法分析以後咱們能夠拿到抽象語法樹AST,接着就能夠對這個樹進行分析,作靜態代碼分析或者無用代碼分析均可以,網上也有不少資料介紹這塊的研究。感興趣的能夠搜索下或者看下 Introduction to the Clang AST
以上內容主要介紹了下iOS編譯相關的知識,若有內容錯誤,歡迎指正。
做爲一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個個人iOS交流羣:789143298 ,無論你是小白仍是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 你們一塊兒交流學習成長!