iOS探索:iOS程序的Build過程

注1:本文由破船譯自The Build Process

注2:

1
2
3
4
5
6
7
8
9
10
本文將輕度解密Xcode build日誌,還原iOS程序build的過程。
另外將介紹如何對build過程的控制,進而定製出自己希望的流程,
例如通過Build phase的定製,給app icon打水印(包括版本號和日期)。

通過對工程文件的解讀,爲你揭開工程文件(.pbxproj)與
build settings的關係。這對於解決工程文件的merge衝突非常有幫助。

PS:實際上各開發平臺的build過程都比較相似,如果你熟悉了
某個平臺的build過程,那麼同樣的原理也適用於別的平臺。
可以說換湯不換藥,本質是一樣的。

下面開始吧:

本文目錄如下所示:

  1. 解密Build日誌
  2. Build過程的控制
  3. 工程文件
  4. 小結

當我們進行開發時,如果需要運行程序,只要在Xcode中點擊運行按鈕(這個按鈕看起來有點像在播放音樂),過一會,我們的程序就會運行在設備或者模擬器上了,看似簡單的操作過程,不過在這背後隱藏了許多步驟!當然,有時候也會遇到一些錯誤。

本文,我將從稍微高一點的角度來解讀整個Build過程,並探索一下Build過程與Xcode界面上顯示的project setting有多大關係。當然,爲了更加深入的瞭解每一步實際執行的任務,我會適當的引入一些別的文章。

解密Build日誌

爲了瞭解Xcode build過程的內部工作原理,我們首先把突破點放在完整的log文件上。打開Log Navigator,從列表中選擇一個Build,Xcode就會通過很漂亮的一種格式將log文件顯示出來。如下圖所示:

默認情況下,XCode會把大量的log信息隱藏起來,你只需要點擊選中某條log,然後點擊右邊的展開按鈕,就能看到該條log的詳細信息了。當然,你也可以選中一條或者多條日誌,然後通過Cmd+C,就能將相關的所有文本信息拷貝到粘貼板上。另外,還可以通過菜單Editor中的Copy transcript for shown results將所有的log信息複製到粘貼板上。

在我這兒的示例中,將近有10000行log信息(當然,大多數信息是由OpenSSL帶來的,並非來自我們的代碼)。下面我們就開始吧!

首先,你可能會發現輸出的log信息,被工程中對應的target分割開了:

1
2
3
4
5
6
7
8
9
10
11
12
13
Build target Pods-SSZipArchive
...
Build target Makefile-openssl
...
Build target Pods-AFNetworking
...
Build target crypto
...
Build target Pods
...
Build target ssl
...
Build target objcio

在我這的工程中有好幾個依賴項:如包含在Pods中的AFNetworking 和 SSZipArchive, 已經以子工程形式存在的OpenSSL等。

針對這裏的每個target,Xcode都會執行一些列的操作,以將相關的源代碼轉換爲機器可讀的二進制(於所選平臺相關)。我們來親密接觸一下第一個targetSSZipArchive吧。

在這個target的log輸出中,我們可以看到每個任務執行的詳細情況。例如,第一個是處理一個預編譯頭文件(爲了增加其可讀性,我省略了許多細節):

1
2
3
4
5
6
7
8
9
10
11
12
13
(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過程中,每個任務都會出現類似上面的這些log信息,我們就通過上面的log信息瞭解詳情吧。

  1. 每個log都會以這樣的一行來對任務進行描述。
  2. 接着下面帶縮進的這3行會被輸出。此處,修改了工作路徑,並對PANG和PATH環境變量進行設置。
  3. 這裏纔是真正煥發出魔力的地方。爲了處理一個.pch文件,調用了clang,並且附帶了大量的選項。這行log信息顯示出了所有的調用參數,我們稍微看幾個參數吧:
  4. -x標示符用來指定語言,此時是objective-c-header
  5. 目標架構指定爲armv7
  6. 標示#defines的內容已經被添加了。
  7. -c標示符用來告訴clang具體如何運行。-c意味着:運行預處理器、詞法分析、類型檢查LLVM的生成和優化,以及特定target相關彙編代碼的生成階段,最後,運行這個彙編代碼以生成.o目標文件。
  8. 輸入文件。
  9. 輸出文件。

雖然有大量的log信息,不過我不會把每個log信息都做詳解。我們的目的是讓你瞭解在build過程中,完整的瞭解什麼工具被調用,以及都使用了什麼參數。

針對這個target,雖然只有一個.pch文件,但實際上這裏對objective-c-header文件處理了兩次。下面來看看log信息告訴我們的詳細情況:

1
2
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 ...

可以看到,build了兩種target:armv7和armv7s,所以clang爲每種架構處理了一次這個文件。

緊接着預編譯頭文件的處理之後,我們可以找到SSZipArchive target相關的其它一些任務:

1
2
3
CompileC ...
Libtool ...
CreateUniversalBinary ...

通過名稱,我們基本能夠知道個大概:CompileC用來編譯.m和.c文件,Libtool根據目標文件創建出一個庫,而CreateUniversalBinary則將上一階段產生的兩個.a文件(對應着兩個不同的架構)合併爲一個通用的二進制文件(可以運行在armv7和armv7s上)。

上面這些類似的步驟會出現在工程中所有其它的依賴項中。

當所有的依賴項都準備好了,就可以開始構建我們程序的target了。針對該target輸出的log信息包含了之前沒有出現過的內容,這些內容非常有價值:

1
2
3
4
5
6
7
8
9
10
11
12
PhaseScriptExecution ...
DataModelVersionCompile ...
Ld ...
GenerateDSYMFile ...
CopyStringsFile ...
CpResource ...
CopyPNGFile ...
CompileAssetCatalog ...
ProcessInfoPlistFile ...
ProcessProductPackaging /.../some-hash.mobileprovision ...
ProcessProductPackaging objcio/objcio.entitlements ...
CodeSign ...

在上面的任務中,可能Ld不能一眼看出是什麼意思,此處它是一個linker工具,跟libtool類似。實際上libtool會簡單的調用ld和lipo。而ld用來構建可執行文件。更多編譯和鏈接相關的文章可以看看 Daniel 和 Chris寫的。

上面這些步驟,實際上都會調用相關的命令行工具來做實際的工作,這跟之前我們看的步驟ProcessPCH類似。至此,我將不會繼續介紹這些log信息了,我將帶來大家從另外一個不同的角度來繼續探索這些任務:Xcode是如何知道哪些任務需要被執行?

Build過程的控制

當你選中在Xcode 5中的一個工程時,project editor會在頂部顯示出6個tabs:General, Capabilities, Info, Build Settings, Build Phases 以及 Build Rules。如下圖所示:

其中最後3項與build過程的相關度最大。

Build Phases

Build Phases代表着將代碼構建爲一個可執行文件的規則。它描述了build過程中必須執行的不同任務。

首先,指定了target的依賴項。這將告訴build系統在當前target可以build之前,必須先build target的依賴項。實際上這並不屬於真正的build phase,在這裏,Xcode只不過將其與build phase顯示到一塊罷了。

接着是一個CocoaPods相關的腳本需要在build phase執行——更多CocoaPods相關信息可以查看Michele的文章

然後在Compile Sources中指定了所有必須進行編譯的文件。更多相關內容我們將在build rules和build settings中研究。在Compile Sources中指定的文件將根據這些rule和setting被處理。

當編譯結束,下一步就是將所有的內容鏈接到一塊:Link Binary with Libraries。在這裏面列出了所有的靜態庫和動態庫,這些庫會與上面編譯階段生成的目標文件進行鏈接。實際上靜態庫和動態庫的處理過程有非常大的區別,相關內容可以參考Daniel的文章 Mach-O executables

當鏈接完成之後,build phase中最後需要處理的就是將靜態資源(例如圖片和字體)拷貝到app bundle中。需要注意的是,如果圖片資源是PNG格式,那麼不僅僅對其進行拷貝,還會做一些優化(如果build settings中的PNG優化是打開的)。

雖然靜態資源的拷貝是build phase中的最後一步,但這並不代表build過程已經完成了。例如,還沒有進行code signing(這並不是build phase考慮的範疇),code signing屬於build步驟中的最後一步Packaging

定製Build Phases

至此,你已經完全可以掌控build phases相關內容(先不考慮默認的設置項),例如,你可以在build phases中添加運行自定義腳本,就像CocoaPods使用的一樣,來做額外的工作。當然也可以添加一些資源的拷貝任務,當你需要將某些確定的資源拷貝到制定的target目錄中,這非常有用。

另外你可以通過定製build phase來添加帶有水印(包括版本號和commit hash)的app icon。只需要在build phase中添加一個Run Script,然後用下面的命令來獲取版本號和commit hash:

1
2
version=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_FILE}"`
commit=`git rev-parse --short HEAD`

然後可以使用ImageMagick來修改app icon。這裏有一個完整的示例,可以參考。

如果你希望編寫的代碼比較簡潔點,那麼可以添加一個Run Script,如果一個源文件超過指定行數,就發出警告。如下代碼所示,設置的行數爲200。

1
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

Build rules指定了不同文件類型該如何編譯。一般來說,開發者並不需要修改這裏面的內容。如果你需要對特定的文件類型添加處理方法,那麼可以在此處天劍一條新的規則。

一條build rule指定了其應用於那種文件類型,該文件類型是如何被處理的,以及輸出內容被放置到何處。比方說,我們創建了一條預處理規則,該規則將Objective-C的實現文件當做輸入,然後解析文件內部的註釋內容,最後再輸出一個.m文件,文件中包含了生成的代碼。由於我們不能將.m文件既當做輸入又當做輸出,所以我使用了.mal後綴,定製的build rule如下所示:

上面的規則應用於所有後綴爲*.mal的文件,這些文件會被自定義的腳本處理(調用我們的預處理器,並附帶上輸入和輸出參數)。最後,該規則告訴build system在哪裏可以找到此規則的輸出文件。

由於這裏的輸出是一個.m文件,那麼build使這些.m文件會被編譯處理(就如剛開始介紹的那些預處理步驟)。

在腳本中,我使用了少量的變量來指定正確的路徑和文件名。在蘋果的Build Setting Reference.文檔中可以找到所有可用的變量。build過程中,要想觀察所有已存在的環境變量,你可以添加一個Run Script build phase,並勾選上Show environment variables in build log

Build Settings

至此,我們已經瞭解到build phases是如何被用來定義build 過程的步驟,以及build rules是如何指定哪些文件類型在編譯階段需要被預處理。在build settings中,我們可以配置每個任務(之前在build log輸出中看到的任務)的詳細內容。

在這裏,你會發現build 過程的每一個階段,都有許多選項:從編譯、鏈接一直到code signing和packaging。注意,settings被分割爲不同的部分,大部分會於build phases有關聯,有時候也會指定編譯的文件類型。

這些選項基本都有不錯的文檔介紹,你可以在右邊面板中的quick help inspector或者 Build Setting Reference中查看到。

工程文件

上面我們介紹的所有內容都被保存在工程文件(.pbxproj)中,除了其它一些工程相關信息(例如file groups),我們很少會深入該文件內部,除非在代碼merge時發生衝突,或許會進去看看。

我建議你用文本編輯器打開一個工程文件,從頭到尾的看一遍裏面的內容。它的可讀性非常高,裏面的許多內容一看就知道什麼意思了,不會存在太大的問題。通過閱讀並完全理解工程文件,這對於合併工程文件的衝突非常有幫助。

首先,我們來看看文件中叫做rootObject的entry。在我的工程中,如下所示:

1
rootObject = 1793817C17A9421F0078255E /* Project object */;

根據這個ID(1793817C17A9421F0078255E),我們可以找到main工程的定義:

1
2
3
4
/* Begin PBXProject section */
    1793817C17A9421F0078255E /* Project object */ = {
        isa = PBXProject;
...

在這部分section中包含了一些keys,順從這些key,我們可以瞭解到更多關於這個工程文件的組成。例如,mainGroup指向了root file group。如果你按照這個思路,你可以快速瞭解到在.pbxproj文件中工程的結構。下面我要來介紹一些與build過程相關的內容。其中target key指向了build target的定義:

1
2
3
4
targets = (
    1793818317A9421F0078255E /* objcio */,
    170E83CE17ABF256006E716E /* objcio Tests */,
);

根據第一個id,我們找到一個target的定義:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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指向了可用的配置項,一般包括DebugRelease。根據debug對應的id,我們可以找到build setting tab中所有選項存儲的位置:

1
2
3
4
5
6
7
8
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屬性則簡單的列出了在Xcode中定義的所有build phases。這非常容易識別出來(Xcode中的參數使用了它們原本真正的名字,並以C風格進行註釋)。

buildRules屬性是空的:因爲在該工程中,我沒有自定義build rules。

dependencies列出了在Xcode build phase tab中列出的target依賴項。

沒那麼嚇人,不是嗎?工程中剩下的內容就留給你去當做練習來了解吧。只需要順着ID走,即可,一旦你找到了敲門,理解了Xcode中工程設置的不同section,那麼對於merge工程文件的衝突時,將變得非常簡單。甚至可以在GitHub中就能閱讀工程文件,而不用將工程文件clone到本地,並用Xcode打開。

小結

當今的軟件是都用其它複雜的一些軟件和資源開發出來的,例如library和build工具等。反過來,這些工具是構建於底層架構的,這猶如剝洋蔥一樣,一層包着一層。雖然這樣一層一層的,給人感覺太複雜,但是你完全可以去深入瞭解它們,這非常有助於你對軟件的深入理解,實際上當你瞭解之後,這並沒有想象中的那麼神奇,只不過它是一層一層堆砌起來的,每一層都是基於下一層構建起來的。

在這裏,我們只是輕微的探究了一下build過程,當我們點擊Xcode中的允許按鈕時,並沒必要深入瞭解內部具體發生了什麼。只需要瞭解到build的過程,以及可控的一些操作順序即可。當然,要想進一步深入瞭解,可以試着閱讀其它一些文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章