iOS Principle:LLVMAndClang

👨🏻‍💻 Github Demohtml

方便記憶:

  • 編譯語言:OC 和 Swift 基於 Clang 和 LLVM 來編譯(Clang 前端、LLVM 後端)
  • 特色:Clang 更快、內存佔用小、兼容GCC、設計簡單、模塊化庫(GCC 支持JAVA等和更多平臺)
  • 編譯工做:Clang 語法分析,語義分析,生成中間代碼;LLVM 機器無關的代碼優化,生成機器語言
  • 編譯完成:生成 dSYM 文件存放函數地址映射,Fabric、友盟等崩潰解析
  • 編譯插入:預處理——宏、插入腳本——cocoapod
  • 編譯時間優化:
    • 代碼層—優化@class代替#import、打包庫、頭文件進行預編譯;
    • 編譯器層—debug模式不生成dsym和開啓Build Active Architecture而且關閉編譯器優化Only

整理學習 iOS Principle 一系列的文章,每篇開頭歸結知識點,幫助記憶前端


一.相關概念

歷史緣由

2000年,伊利諾伊大學厄巴納-香檳分校(University of Illinois at Urbana-Champaign 簡稱UIUC)這所享有世界聲望的一流公立研究型大學的 Chris Lattner(他的 twitter @clattner_llvm ) 開發了一個叫做 Low Level Virtual Machine 的編譯器開發工具套件,後來涉及範圍愈來愈大,能夠用於常規編譯器,JIT編譯器,彙編器,調試器,靜態分析工具等一系列跟編程語言相關的工做,因而就把簡稱 LLVM 這個簡稱做爲了正式的名字。Chris Lattner 後來又開發了 Clang,使得 LLVM 直接挑戰 GCC 的地位。2012年,LLVM 得到美國計算機學會 ACM 的軟件系統大獎,和 UNIX,WWW,TCP/IP,Tex,JAVA 等齊名。linux

Chris Lattner 生於 1978 年,2005年加入蘋果,將蘋果使用的 GCC 全面轉爲 LLVM。2010年開始主導開發 Swift 語言。git

iOS 開發中 Objective-C 是 Clang / LLVM 來編譯的。github

Swift 是 Swift / LLVM,其中 Swift 前端會多出 SIL optimizer,它會把 .swift 生成爲中間代碼 .sil 屬於 High-Level IR, 由於 Swift 在編譯時就完成了方法綁定,直接經過地址調用屬於強類型語言,方法調用再也不是像OC那樣的消息發送,這樣編譯就能夠得到更多的信息用在後面的後端優化上。正則表達式

LLVM是一個模塊化和可重用的編譯器和工具鏈技術的集合,Clang 是 LLVM 的子項目,是 C,C++ 和 Objective-C 編譯器,目的是提供驚人的快速編譯,比 GCC 快3倍,其中的 clang static analyzer 主要是進行語法分析,語義分析和生成中間代碼,固然這個過程會對代碼進行檢查,出錯的和須要警告的會標註出來。LLVM 核心庫提供一個優化器,對流行的 CPU 作代碼生成支持。lld 是 Clang / LLVM 的內置連接器,clang 必須調用連接器來產生可執行文件。objective-c

這裏是 Clang 官方詳細文檔: Welcome to Clang’s documentation! — Clang 4.0 documentation 編程

這篇是對 LLVM 架構的一個概述: The Architecture of Open Source Applicationsswift

將編譯器以前對於編譯的前世此生也是須要了解的,好比回答下這個問題,編譯器程序是用什麼編譯的?看看 《linkers and loaders》這本書就知道了。後端


LLVM 與 Clang 介紹

LLVM 是 Low Level Virtual Machine 的簡稱,這個庫提供了與編譯器相關的支持,可以進行程序語言的編譯期優化、連接優化、在線編譯優化、代碼生成。簡而言之,能夠做爲多種語言編譯器的後臺來使用。若是這樣還比較抽象的話,介紹下 Clang 就知道了:Clang 是一個 C++ 編寫、基於 LLVM、發佈於 LLVM BSD 許可證下的 C/C++/Objective C/Objective C++ 編譯器,其目標(之一)就是超越 GCC。

Clang 開發事出有因,Wiki 介紹以下:

Apple 使用 LLVM 在不支持所有 OpenGL 特性的 GPU (Intel 低端顯卡) 上生成代碼 (JIT),令程序仍然可以正常運行。以後 LLVM 與 GCC 的集成過程引起了一些不快,GCC 系統龐大而笨重,而 Apple 大量使用的 Objective-C 在 GCC 中優先級很低。此外 GCC 做爲一個純粹的編譯系統,與 IDE 配合不好。加之許可證方面的要求,Apple 沒法使用修改版的 GCC 而閉源。因而 Apple 決定從零開始寫 C family 的前端,也就是基於 LLVM 的 Clang 了。

Clang 的特性:

  • 快:經過編譯 OS X 上幾乎包含了全部 C 頭文件的 carbon.h 的測試,包括預處理 (Preprocess),語法 (lex),解析 (parse),語義分析 (Semantic Analysis),抽象語法樹生成 (Abstract Syntax Tree) 的時間,Clang 是 Apple GCC 4.0 的 2.5x 快。(2007-7-25)
  • 內存佔用小:Clang 內存佔用是源碼的 130%,Apple GCC 則超過 10x。
  • 診斷信息可讀性強:我不會排版,推薦去網站觀看。其中錯誤的語法不但有源碼提示,還會在錯誤的調用和相關上下文的下方有~~~~~和^的提示,相比之下 GCC 的提示很天書。
  • GCC 兼容性。
  • 設計清晰簡單,容易理解,易於擴展加強。與代碼基礎古老的 GCC 相比,學習曲線平緩。
  • 基於庫的模塊化設計,易於 IDE 集成及其餘用途的重用。因爲歷史緣由,GCC 是一個單一的可執行程序編譯器,其內部完成了從預處理到最後代碼生成的所有過程,中間諸多信息都沒法被其餘程序重用。Clang 將編譯過程分紅彼此分離的幾個階段,AST 信息可序列化。經過庫的支持,程序可以獲取到 AST 級別的信息,將大大加強對於代碼的操控能力。對於 IDE 而言,代碼補全、重構是重要的功能,然而若是沒有底層的支持,只使用 tags 分析或是正則表達式匹配是很難達成的。

固然,GCC 也有其優點:

  • 支持 JAVA/ADA/FORTRAN
  • 當前的 Clang 的 C++ 支持落後於 GCC,參見。(近日 Clang 已經能夠自編譯,見)
  • GCC 支持更多平臺
  • GCC 更流行,普遍使用,支持完備
  • GCC 基於 C,不須要 C++ 編譯器便可編譯

iOS 開發中用途

通常能夠將編程語言分爲兩種,編譯語言和直譯式語言。

  • 編譯語言:像C++,Objective C都是編譯語言。編譯語言在執行的時候,必須先經過編譯器生成機器碼,機器碼能夠直接在CPU上執行,因此執行效率較高。
  • 直譯式語言:像JavaScript,Python都是直譯式語言。直譯式語言不須要通過編譯的過程,而是在執行的時候經過一箇中間的解釋器將代碼解釋爲CPU能夠執行的代碼。因此,較編譯語言來講,直譯式語言效率低一些,可是編寫的更靈活,也就是爲啥JS大法好。

iOS開發目前的經常使用語言是:Objective和Swift。兩者都是編譯語言,換句話說都是須要編譯才能執行的。兩者的編譯都是依賴於Clang + LLVM.


iOS編譯

無論是OC仍是Swift,都是採用Clang做爲編譯器前端,LLVM(Low level vritual machine)做爲編譯器後端。因此簡單的編譯過程如圖


編譯器前端

編譯器前端的任務是進行:語法分析,語義分析,生成中間代碼(intermediate representation )。在這個過程當中,會進行類型檢查,若是發現錯誤或者警告會標註出來在哪一行。


編譯器後端

編譯器後端會進行機器無關的代碼優化,生成機器語言,而且進行機器相關的代碼優化。iOS的編譯過程,後端的處理以下

  • LVVM優化器會進行BitCode的生成,連接期優化等等。

  • LLVM機器碼生成器會針對不一樣的架構,好比arm64等生成不一樣的機器碼。


執行一次 XCode build 的流程

當你在XCode中,選擇build的時候(快捷鍵command+B),會執行以下過程

  • 編譯信息寫入輔助文件,建立編譯後的文件架構(name.app)
  • 處理文件打包信息,例如在debug環境下
Entitlements:
{
   "application-identifier" = "app的bundleid";
   "aps-environment" = development;
}
複製代碼
  • 執行CocoaPod編譯前腳本(例如對於使用CocoaPod的工程會執行CheckPods Manifest.lock)
  • 編譯各個.m文件,使用CompileC和clang命令。
CompileC ClassName.o ClassName.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler
export LANG=en_US.US-ASCII
export PATH="..."
clang -x objective-c -arch x86_64 -fmessage-length=0 -fobjc-arc... -Wno-missing-field-initializers ... -DDEBUG=1 ... -isysroot iPhoneSimulator10.1.sdk -fasm-blocks ... -I 上文提到的文件 -F 所須要的Framework  -iquote 所須要的Framework  ... -c ClassName.c -o ClassName.o
複製代碼

經過這個編譯的命令,咱們能夠看到

  • clang是實際的編譯命令
  • x objective-c 指定了編譯的語言
  • arch x86_64制定了編譯的架構,相似還有arm7等
  • fobjc-arc 一些列-f開頭的,指定了採用arc等信息。這個也就是爲何你能夠對單獨的一個.m文件採用非ARC編程。
  • Wno-missing-field-initializers 一系列以-W開頭的,指的是編譯的警告選項,經過這些你能夠定製化編譯選項
  • DDEBUG=1 一些列-D開頭的,指的是預編譯宏,經過這些宏能夠實現條件編譯
  • iPhoneSimulator10.1.sdk 制定了編譯採用的iOS SDK版本
  • I 把編譯信息寫入指定的輔助文件
  • F 連接所須要的Framework
  • c ClassName.c 編譯文件
  • o ClassName.o 編譯產物

工做流程

  • 連接須要的Framework,例如Foundation.framework,AFNetworking.framework,ALiPay.fframework
  • 編譯xib文件
  • 拷貝xib,圖片等資源文件到結果目錄
  • 編譯ImageAssets
  • 處理info.plist
  • 執行CocoaPod腳本
  • 拷貝Swift標準庫
  • 建立.app文件和對其簽名

dSYM 文件

咱們在每次編譯事後,都會生成一個dsym文件。dsym文件中,存儲了16進制的函數地址映射。

在App實際執行的二進制文件中,是經過地址來調用方法的。在App crash的時候,第三方工具(Fabric,友盟等)會幫咱們抓到崩潰的調用棧,調用棧裏會包含crash地址的調用信息。而後,經過dSYM文件,咱們就能夠由地址映射到具體的函數位置。

XCode中,選擇Window -> Organizer能夠看到咱們生成的archier文件

iOS 如何調試第三方統計到的崩潰報告 (http://blog.csdn.net/hello_hwc/article/details/50036323)


attribute

或多或少,你都會在第三方庫或者iOS的頭文件中,見到過attribute。

好比

__attribute__ ((warn_unused_result)) //若是沒有使用返回值,編譯的時候給出警告
複製代碼

attribtue 是一個高級的的編譯器指令,它容許開發者指定更更多的編譯檢查和一些高級的編譯期優化。

分爲三種:

  • 函數屬性 (Function Attribute)
  • 類型屬性 (Variable Attribute )
  • 變量屬性 (Type Attribute )

語法結構

attribute 語法格式爲:attribute ((attribute-list))

放在聲明分號「;」前面。

好比,在三方庫中最多見的,聲明一個屬性或者方法在當前版本棄用了

@property (strong,nonatomic)CLASSNAME * property __deprecated;
複製代碼

這樣的好處是:給開發者一個過渡的版本,讓開發者知道這個屬性被棄用了,應當使用最新的API,可是被__deprecated的屬性仍然能夠正常使用。若是直接棄用,會致使開發者在更新Pod的時候,代碼沒法運行了。

__attribtue__的使用場景不少,本文只列舉iOS開發中經常使用的幾個:

//棄用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)))
複製代碼

Clang警告處理

你必定還見過以下代碼:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
///代碼
#pragma clang diagnostic pop
複製代碼

這段代碼的做用是

  • 對當前編譯環境進行壓棧
  • 忽略-Wundeclared-selector(未聲明的)Selector警告
  • 編譯代碼
  • 對編譯環境進行出棧

經過clang diagnostic push/pop,你能夠靈活的控制代碼塊的編譯選項。

  • iOS 合理利用Clang警告來提升代碼質量 (http://blog.csdn.net/Hello_Hwc/article/details/46425503)

預處理

所謂預處理,就是在編譯以前的處理。預處理可以讓你定義編譯器變量,實現條件編譯。

好比,這樣的代碼很常見

#ifdef DEBUG
//...
#else
//...
#endif
複製代碼

一樣,咱們一樣也能夠定義其餘預處理變量,在XCode-選中Target-build settings中,搜索proprecess。而後點擊圖中藍色的加號,能夠分別爲debug和release兩種模式設置預處理宏。

好比咱們加上:TestServer,表示在這個宏中的代碼運行在測試服務器

而後,配合多個Target(右鍵Target,選擇Duplicate),單獨一個Target負責測試服務器。這樣咱們就不用每次切換測試服務器都要修改代碼了。

#ifdef TESTMODE
//測試服務器相關的代碼
#else
//生產服務器相關代碼
#endif
複製代碼

插入腳本

一般,若是你使用CocoaPod來管理三方庫,那麼你的Build Phase是這樣子的:

其中:[CP]開頭的,就是CocoaPod插入的腳本。

  • Check Pods Manifest.lock,用來檢查cocoapod管理的三方庫是否須要更新
  • Embed Pods Framework,運行腳原本連接三方庫的靜態/動態庫
  • Copy Pods Resources,運行腳原本拷貝三方庫的資源文件

而這些配置信息都存儲在這個文件(.xcodeprog)裏

到這裏,CocoaPod的原理也就大體搞清楚了,經過修改xcodeproject,而後配置編譯期腳本,來保證三方庫可以正確的編譯鏈接。

一樣,咱們也能夠插入本身的腳本,來作一些額外的事情。好比,每次進行archive的時候,咱們都必須手動調整target的build版本,若是一不當心,就會忘記。這個過程,咱們能夠經過插入腳本自動化。

buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}")
buildNumber=$(($buildNumber + 1))
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${PROJECT_DIR}/${INFOPLIST_FILE}"
複製代碼

這段腳本其實很簡單,讀取當前pist的build版本號,而後對其加一,從新寫入。

使用起來也很簡單:

  • Xcode – 選中Target – 選中build phase
  • 選擇添加Run Script Phase

而後把這段腳本拷貝進去,而且勾選Run Script Only When installing,保證只有咱們在安裝到設備上的時候,纔會執行這段腳本。重命名腳本的名字爲Auto Increase build number

而後,拖動這個腳本的到Link Binary With Libraries下面


腳本編譯打包

腳本化編譯打包對於CI(持續集成)來講,十分有用。iOS開發中,編譯打包必備的兩個命令是:

//編譯成.app
xcodebuild  -workspace $projectName.xcworkspace -scheme $projectName  -configuration $buildConfig clean build SYMROOT=$buildAppToDir
//打包
xcrun -sdk iphoneos PackageApplication -v $appDir/$projectName.app -o $appDir/$ipaName.ipa

經過info命令,能夠查看到詳細的文檔
info xcodebuild
複製代碼

以前寫的一套基於 Python 的編譯打包腳本 (https://github.com/ReverseScale/AutoBuildScript/blob/master/autobuild.py)


提升項目編譯速度

一般,當項目很大,源代碼和三方庫引入不少的時候,咱們會發現編譯的速度很慢。在瞭解了XCode的編譯過程後,咱們能夠從如下角度來優化編譯速度:

1)查看編譯時間

咱們須要一個途徑,可以看到編譯的時間,這樣纔能有個對比,知道咱們的優化究竟有沒有效果。

對於XCode 8,關閉XCode,終端輸入如下指令

defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
複製代碼

而後,重啓XCode,而後編譯,你會在這裏看到編譯時間。

2)代碼層面的優化

2.1)forward declaration

所謂forward declaration,就是@class CLASSNAME,而不是#import CLASSNAME.h。這樣,編譯器能大大提升#import的替換速度。

2.2)對經常使用的工具類進行打包(Framework/.a)

打包成Framework或者靜態庫,這樣編譯的時候這部分代碼就不須要從新編譯了。

2.3)經常使用頭文件放到預編譯文件裏

XCode的pch文件是預編譯文件,這裏的內容在執行XCode build以前就已經被預編譯,而且引入到每個.m文件裏了。

3)編譯器選項優化

3.1)Debug模式下,不生成dsym文件

上文提到了,dysm文件裏存儲了調試信息,在Debug模式下,咱們能夠藉助XCode和LLDB進行調試。因此,不須要生成額外的dsym文件來下降編譯速度。

3.2)Debug開啓Build Active Architecture Only

在XCode -> Build Settings -> Build Active Architecture Only 改成YES。這樣作,能夠只編譯當前的版本,好比arm7/arm64等等,記得只開啓Debug模式。這個選項在高版本的XCode中自動開啓了。

3.3)Debug模式下,關閉編譯器優化

編譯器優化


更多深刻學習

關於 iOS 編譯 Clang LLVM 相關的知識整理參見: 深刻剖析 iOS 編譯 Clang LLVM


版權聲明

此係列文章內容多爲網上資料整理,文章結尾會列出參照連接,若有紕漏歡迎討論🤗

以上文章整理自:https://my.oschina.net/u/2345393/blog/820141,https://linuxtoy.org/archives/llvm-and-clang.html,https://blog.csdn.net/hello_hwc/article/details/53557308,https://github.com/ming1016/study/wiki/深刻剖析-iOS-編譯-Clang---LLVM

相關文章
相關標籤/搜索