近些日子我們被寵壞了 -- 我們只需要單擊 Xcode 中的一個(gè)按鈕(這個(gè)按鈕看起來(lái)有點(diǎn)像是在播放一些音樂(lè)的動(dòng)作),過(guò)幾秒鐘之后,我們的程序就會(huì)運(yùn)行起來(lái)了,除非遇到一些錯(cuò)誤,這非常的神奇。
在本文中,我們將從更高級(jí)別的角度來(lái)解讀 Build 過(guò)程,并探索一下在 Xcode 界面中暴露出的 project setting 信息與 Build 過(guò)程有什么關(guān)系。為了更加深入的探索 Build 過(guò)程中,每一步實(shí)際執(zhí)行的工作,我都會(huì)在本文中引入一些別的文章。
為了了解 Xcode build 過(guò)程的內(nèi)部工作原理,我們首先把突破口瞄準(zhǔn)完整的 log 文件上。打開(kāi) Log Navigator ,從列表中選擇一個(gè) Build ,Xcode 會(huì)將 log 文件很完美的展現(xiàn)出來(lái)。
http://wiki.jikexueyuan.com/project/objc/images/6-1.png" alt="" />
默認(rèn)情況下,上面的 Xcode 界面中隱藏了大量的信息,我們通過(guò)選擇任務(wù),然后點(diǎn)擊右邊的展開(kāi)按鈕,就能看到每個(gè)任務(wù)的詳細(xì)信息。另外一種可選的方案就是選中列表中的一個(gè)或者多個(gè)任務(wù),然后選擇組合鍵 Cmd-C,這將會(huì)把所有的純文本信息拷貝至粘貼板。最后,我們還可以選擇 Editor 菜單中的 "Copy transcript for shown results",以此將所有的 log 信息拷貝到粘貼板中。
本文給出的示例中,log 信息將近有 10,000 行(其實(shí)大多數(shù)的 log 信息是編譯 OpenSSL 時(shí)生成的,并不是我們自己所寫(xiě)的代碼生成的)。下面我們就開(kāi)始吧!
注意觀察輸出的 log 信息,首先會(huì)發(fā)現(xiàn) log 信息被分為不同的幾大塊,它們與我們工程中的targets相互對(duì)應(yīng)著:
Build target Pods-SSZipArchive
...
Build target Makefile-openssl
...
Build target Pods-AFNetworking
...
Build target crypto
...
Build target Pods
...
Build target ssl
...
Build target objcio
本文涉及到的工程有幾個(gè)依賴項(xiàng):其中 AFNetworking 和 SSZipArchive 包含在 Pods 中,而 OpenSSL 則以子工程的形式包含在工程中。
針對(duì)工程中的每個(gè) target,Xcode 都會(huì)執(zhí)行一系列的操作,將相關(guān)的源碼,根據(jù)所選定的平臺(tái),轉(zhuǎn)換為機(jī)器可讀的二進(jìn)制文件。下面我們?cè)敿?xì)的了解一下第一個(gè) target:SSZipArchive。
在針對(duì)這個(gè) target 輸出的 log 信息中,我們可以看到每個(gè)任務(wù)被執(zhí)行的詳細(xì)情況。例如第一個(gè)任務(wù)是處理一個(gè)預(yù)編譯頭文件(為了增強(qiáng) log 信息的可讀性,我省略了許多細(xì)節(jié)):
(1) ProcessPCH /.../Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7 objective-c com.apple.compilers.llvm.clang.1_0.compiler
(2) cd /.../Dev/objcio/Pods
setenv LANG en_US.US-ASCII
setenv PATH "..."
(3) /.../Xcode.app/.../clang
(4) -x objective-c-header
(5) -arch armv7
... configuration and warning flags ...
(6) -DDEBUG=1 -DCOCOAPODS=1
... include paths and more ...
(7) -c
(8) /.../Pods-SSZipArchive-prefix.pch
(9) -o /.../Pods-SSZipArchive-prefix.pch.pch
在 build 處理過(guò)程中,每個(gè)任務(wù)都會(huì)出現(xiàn)類似上面的這些 log 信息,我們就通過(guò)上面的 log 信息進(jìn)一步了解詳情。
.pch
文件,調(diào)用了 clang,并附帶了許多可選項(xiàng)。下面跟著輸出的 log 信息顯示了完整的調(diào)用過(guò)程,以及所有的參數(shù)。我們看看其中的幾個(gè)參數(shù)...-x
標(biāo)示符用來(lái)指定所使用的語(yǔ)言,此處是 objective-c-header
。armv7
。#defines
的內(nèi)容已經(jīng)被添加了。-c
標(biāo)示符用來(lái)告訴 clang 具體該如何做。-c
表示:運(yùn)行預(yù)處理器、詞法分析器、類型檢查、LLVM 的生成和優(yōu)化,以及 target 指定匯編代碼的生成階段,最后,運(yùn)行匯編器以產(chǎn)出一個(gè).o
的目標(biāo)文件。雖然有大量的 log 信息,不過(guò)我不會(huì)對(duì)每個(gè)任務(wù)做詳細(xì)的介紹。我們的重點(diǎn)是讓你全面的了解在整個(gè) build 過(guò)程中,哪些工具會(huì)被調(diào)用,以及背后會(huì)使用到了哪些參數(shù)。
針對(duì)這個(gè) target ,雖然只有一個(gè) .pch
文件,但實(shí)際上這里對(duì) objective-c-header
文件的處理有兩個(gè)任務(wù)。通過(guò)觀察具體輸出的 log 信息,我們可以知道詳情:
ProcessPCH /.../Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7 objective-c ...
ProcessPCH /.../Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7s objective-c ...
從上面的 log 信息中,可以明顯的看出 target 針對(duì)兩種架構(gòu)做了 build -- armv7 和 armv7s -- 因此 clang 對(duì)文件做了兩次處理,每次針對(duì)一種架構(gòu)。
在處理預(yù)編譯頭文件之后,可以看到針對(duì) SSZipArchive target 有另外的幾個(gè)任務(wù)類型。
CompileC ...
Libtool ...
CreateUniversalBinary ...
顧名思義:CompileC
用來(lái)編譯 .m
和 .c
文件,Libtool
用來(lái)從目標(biāo)文件中構(gòu)建 library,而 CreateUniversalBinary
則將上一階段產(chǎn)生的兩個(gè) .a
文件(每個(gè)文件對(duì)應(yīng)一種架構(gòu))合并為一個(gè)通用的二進(jìn)制文件,這樣就能同時(shí)在 armv7 和 armv7s 上面運(yùn)行。
接著,在工程中其它一些依賴項(xiàng)也會(huì)發(fā)生于此類似的步驟。AFNetworking 被編譯之后,會(huì)與 SSZipArchive 進(jìn)行鏈接,以當(dāng)做 pod library。OpenSSL 編譯之后,會(huì)接著處理 crypto 和 ssl target。
當(dāng)所有的依賴項(xiàng)都 build 完成之后,就輪到我們程序的 target 了。Build 該 target 時(shí),輸出的 log 信息會(huì)包含一些非常有價(jià)值,并且之前沒(méi)有出現(xiàn)過(guò)的內(nèi)容:
PhaseScriptExecution ...
DataModelVersionCompile ...
Ld ...
GenerateDSYMFile ...
CopyStringsFile ...
CpResource ...
CopyPNGFile ...
CompileAssetCatalog ...
ProcessInfoPlistFile ...
ProcessProductPackaging /.../some-hash.mobileprovision ...
ProcessProductPackaging objcio/objcio.entitlements ...
CodeSign ...
在上面的任務(wù)列表中,根據(jù)名稱不能區(qū)分的唯一任務(wù)可能就是 Ld
,Ld
是一個(gè) linker 工具的名稱,與 libtool
非常相似。實(shí)際上,libtool
也是簡(jiǎn)單的調(diào)用 ld
和 lipo
。'ld'被用來(lái)構(gòu)建可執(zhí)行文件,而libtool
則用來(lái)構(gòu)建 library 文件。閱讀Daniel 和 Chris兩篇文章,可以了解到更多關(guān)于編譯和鏈接的工作原理。
上面每一個(gè)步驟,實(shí)際上都會(huì)調(diào)用相關(guān)的命令行工具來(lái)做實(shí)際的工作,這跟之前我們看到的的 ProcessPCH
類似。至此,我將不會(huì)繼續(xù)介紹這些 log 信息了,我將帶領(lǐng)大家從另外一個(gè)不同的角度來(lái)繼續(xù)探索這些任務(wù):Xcode 是如何知道哪些任務(wù)需要被執(zhí)行?
當(dāng)你選擇 Xcode 5 中的一個(gè)工程時(shí),會(huì)在 project editor 頂部顯示出 6 個(gè) tabs:General, Capabilities, Info, Build Settings, Build Phases 以及 Build Rules。
http://wiki.jikexueyuan.com/project/objc/images/6-2.png" alt="" />
對(duì)于我們理解 build 過(guò)程來(lái)說(shuō),其中最后 3 項(xiàng)與 build 過(guò)程緊密相連。
Build Phases 代表著將代碼轉(zhuǎn)變?yōu)榭蓤?zhí)行文件的最高級(jí)別規(guī)則。里面描述了 build 過(guò)程中必須執(zhí)行的不同類型規(guī)則。
http://wiki.jikexueyuan.com/project/objc/images/6-3.png" alt="" />
首先是 target 依賴項(xiàng)的構(gòu)建。這里會(huì)告訴 build 系統(tǒng),build 當(dāng)前的 target 之前,必須先對(duì)這里的依賴性進(jìn)行 build。實(shí)際上這并不屬于真正的 build phase,在這里,Xcode 只不過(guò)將其與 build phase 顯示到一塊罷了。
接著在 build phase中是一個(gè) CocoaPods 相關(guān)的腳本 script execution,接著在 Compile Sources
section 中規(guī)定了所有必須參與編譯的文件。需要留意的是,這里并沒(méi)有指明這些文件是如何被編譯處理的。關(guān)于處理這些文件的更多內(nèi)容,我們將在研究 build rules 和 build settings 時(shí)學(xué)習(xí)到。此處列出的所有文件將根據(jù)相關(guān)的 rules 和 settings 被處理。
當(dāng)編譯結(jié)束之后,接下來(lái)就是將編譯所生成的目標(biāo)文件鏈接到一塊。注意觀察,Xcode 中的 build phase 之后是:"Link Binary with Libraries." 這里面列出了所有的靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù),這些庫(kù)會(huì)參與上面編譯階段生成的目標(biāo)文件進(jìn)行鏈接。靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)的處理過(guò)程有非常大的區(qū)別,相關(guān)內(nèi)容請(qǐng)參考 Daniel的文章 Mach-O 可執(zhí)行文件。
當(dāng)鏈接完成之后,build phase 中最后需要處理的就是將靜態(tài)資源(例如圖片和字體)拷貝到 app bundle 中。需要注意的是,如果圖片資源是PNG格式,那么不僅僅對(duì)其進(jìn)行拷貝,還會(huì)做一些優(yōu)化(如果 build settings 中的 PNG 優(yōu)化是打開(kāi)的話)。
雖然靜態(tài)資源的拷貝是 build phase 中的最后一步,但 build 還沒(méi)有完成。例如,還沒(méi)有進(jìn)行 code signing (這并不是 build phase 考慮的范疇),code signing 屬于 build 步驟中的最后一步 "Packaging"。
至此,如果不考慮默認(rèn)設(shè)置的話,你已經(jīng)可以完全掌握了上面介紹的 build phases。例如,你可以在 build phases 中添加運(yùn)行自定義腳本,就像CocoaPods使用的一樣,來(lái)做額外的工作。當(dāng)然也可以添加一些資源的拷貝任務(wù),當(dāng)你需要將某些確定的資源拷貝到指定的 target 目錄中,這非常有用。
另外定制 build phases 有一個(gè)非常好用的功能:添加帶有水?。òò姹咎?hào)和 commit hash)的 app icon -- 只需要在 build phase 中添加一個(gè) "Run Script",并用下面的命令來(lái)獲取版本號(hào)和 commit hash:
version=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_FILE}"`
commit=`git rev-parse --short HEAD`
然后使用 ImageMagick 來(lái)修改 app icon。這里有一個(gè)完整的示例,可以參考。
如果你希望自己或者別人編寫(xiě)的代碼看起來(lái)比較簡(jiǎn)潔點(diǎn),可以添加一個(gè) "Run Script":如果一個(gè)源文件超過(guò)指定行數(shù),就發(fā)出警告。如下代碼所示,設(shè)置的行數(shù)為 200。
find "${SRCROOT}" \( -name "*.h" -or -name "*.m" \) -print0 | xargs -0 wc -l | awk '$1 > 200 && $2 != "total" { print $2 ":1: warning: file more than 200 lines" }'
Build rules 指定了不同的文件類型該如何編譯。一般來(lái)說(shuō),開(kāi)發(fā)者并不需要修改這里面的內(nèi)容。如果你需要對(duì)特定類型的文件添加處理方法,那么可以在此處添加一條新的規(guī)則。
一條 build rule 指定了其應(yīng)用于哪種類型文件,該類型文件是如何被處理的,以及輸出的內(nèi)容該如何處置。比方說(shuō),我們創(chuàng)建了一條預(yù)處理規(guī)則,該規(guī)則將 Objective-C 的實(shí)現(xiàn)文件當(dāng)做輸入,解析文件中的注釋內(nèi)容,最后再輸出一個(gè) .m
文件,文件中包含了生成的代碼。由于我們不能將 .m
文件既當(dāng)做輸入又當(dāng)做輸出,所以我使用了 .mal
后綴,定制的 build rule 如下所示:
http://wiki.jikexueyuan.com/project/objc/images/6-4.png" alt="" />
上面的規(guī)則應(yīng)用于所有后綴為 *.mal
的文件,這些文件會(huì)被自定義的腳本處理(調(diào)用我們的預(yù)處理器,并附帶上輸入和輸出參數(shù))。最后,該規(guī)則告訴 build system 在哪里可以找到此規(guī)則的輸出文件。
在腳本中,我使用了少量的變量來(lái)指定正確的路徑和文件名。在蘋(píng)果的 Build Setting Reference 文檔中可以找到所有可用的變量。build 過(guò)程中,要想觀察所有已存在的環(huán)境變量,你可以在 build phase 中添加一個(gè) "Run Script",并勾選上 "Show environment variables in build log"。
至此,我們已經(jīng)了解到在 build phases 中是如何定義 build 處理的過(guò)程,以及 build rules 是如何指定哪些文件類型在編譯階段需要被預(yù)處理。在 build settings 中,我們可以配置每個(gè)任務(wù)(之前在 build log 輸出中看到的任務(wù))的詳細(xì)內(nèi)容。
你會(huì)發(fā)現(xiàn) build 過(guò)程的每一個(gè)階段,都有許多選項(xiàng):從編譯、鏈接一直到 code signing 和 packaging。注意,settings 是如何被分割為不同的部分 -- 其實(shí)這大部分會(huì)與 build phases 有關(guān)聯(lián),有時(shí)候也會(huì)指定編譯的文件類型。
這些選項(xiàng)基本都有很好的文檔介紹,你可以在右邊面板中的 quick help inspector 或者 Build Setting Reference 中查看到。
上面我們介紹的所有內(nèi)容都被保存在工程文件(.pbxproj
)中,除了其它一些工程相關(guān)信息(例如 file groups),我們很少會(huì)深入該文件內(nèi)部,除非在代碼 merge 時(shí)發(fā)生沖突,或許會(huì)進(jìn)去看看。
建議你用文本編輯器打開(kāi)一個(gè)工程文件,從頭到尾看一遍里面的內(nèi)容。它的可讀性非常高,里面的許多內(nèi)容一看就知道什么意思了,不會(huì)存在太大的問(wèn)題。通過(guò)閱讀并完全理解工程文件,這對(duì)于合并工程文件的沖突非常有幫助。
首先,我們來(lái)看看文件中叫做 rootObject
的條目。在我的工程中,如下所示:
rootObject = 1793817C17A9421F0078255E /* Project object */;
根據(jù)這個(gè) ID(1793817C17A9421F0078255E
),我們可以找到 main 工程的定義:
/* Begin PBXProject section */
1793817C17A9421F0078255E /* Project object */ = {
isa = PBXProject;
...
在這部分中有一些 keys,順從這些 key,我們可以了解到更多關(guān)于這個(gè)工程文件的組成。例如,mainGroup
指向了 root file group。如果你按照這個(gè)思路,你可以快速了解到在 .pbxproj
文件中工程的結(jié)構(gòu)。下面我要來(lái)介紹一些與 build 過(guò)程相關(guān)的內(nèi)容。其中 target
key 指向了 build target 的定義:
targets = (
1793818317A9421F0078255E /* objcio */,
170E83CE17ABF256006E716E /* objcio Tests */,
);
根據(jù)第一個(gè)內(nèi)容,我們找到一個(gè) target 的定義:
1793818317A9421F0078255E /* objcio */ = {
isa = PBXNativeTarget;
buildConfigurationList = 179381B617A9421F0078255E /* Build configuration list for PBXNativeTarget "objcio" */;
buildPhases = (
F3EB8576A1C24900A8F9CBB6 /* Check Pods Manifest.lock */,
1793818017A9421F0078255E /* Sources */,
1793818117A9421F0078255E /* Frameworks */,
1793818217A9421F0078255E /* Resources */,
FF25BB7F4B7D4F87AC7A4265 /* Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
1769BED917CA8239008B6F5D /* PBXTargetDependency */,
1769BED717CA8236008B6F5D /* PBXTargetDependency */,
);
name = objcio;
productName = objcio;
productReference = 1793818417A9421F0078255E /* objcio.app */;
productType = "com.apple.product-type.application";
};
其中 buildConfigurationList
指向了可用的配置項(xiàng),一般是 Debug
和 Release
。根據(jù) debug 對(duì)應(yīng)的 id,我們可以找到 build setting tab 中所有選項(xiàng)存儲(chǔ)的位置:
179381B717A9421F0078255E /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 05D234D6F5E146E9937E8997 /* Pods.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = YES;
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
CODE_SIGN_ENTITLEMENTS = objcio/objcio.entitlements;
...
buildPhases
屬性則簡(jiǎn)單的列出了在 Xcode 中定義的所有 build phases。這非常容易識(shí)別出來(lái)(Xcode 中的參數(shù)使用了它們?cè)菊嬲拿?,并?C 風(fēng)格進(jìn)行注釋)。buildRules
屬性是空的:因?yàn)樵谠摴こ讨?,我沒(méi)有自定義 build rules。dependencies
列出了在 Xcode build phase tab 中列出的 target 依賴項(xiàng)。
沒(méi)那么嚇人,不是嗎?工程中剩下的內(nèi)容就留給你去當(dāng)做練習(xí)來(lái)了解吧。只需要順著對(duì)象的 ID 走,即可,一旦你找到了敲門(mén),理解了Xcode中工程設(shè)置的不同 section ,那么對(duì)于 merge 工程文件的沖突時(shí),將變得非常簡(jiǎn)單。甚至可以在 GitHub 中就能閱讀工程文件,而不用將工程文件 clone 到本地,并用 Xcode 打開(kāi)。
當(dāng)今的軟件是都用其它復(fù)雜的一些軟件和資源開(kāi)發(fā)出來(lái)的,例如 library 和 build 工具等。反過(guò)來(lái),這些工具是構(gòu)建于底層架構(gòu)的,這猶如剝洋蔥一樣,一層包著一層。雖然這樣一層一層的,給人感覺(jué)太復(fù)雜,但是你完全可以去深入了解它們,這非常有助于你對(duì)軟件的深入理解,實(shí)際上當(dāng)你了解之后,這并沒(méi)有想象中的那么神奇,只不過(guò)它是一層一層堆砌起來(lái)的,每一層都是基于下一層構(gòu)建起來(lái)的。
本文所探索 build system 的內(nèi)部機(jī)制猶如剝掉洋蔥的一層。其實(shí)當(dāng)我們點(diǎn)擊 Xcode 中的運(yùn)行按鈕時(shí),我們并沒(méi)必要理解這個(gè)動(dòng)作涉及到的所有內(nèi)容。我們只是深入理解某一層,然后找到一個(gè)有組織的、并且可控的調(diào)用其它工具的順序,如果我們?cè)敢獾脑?,可以做進(jìn)一步的探索。我建議你閱讀本期中的其它文章,以進(jìn)一步了解這個(gè)洋蔥的下一層內(nèi)容!