iOS 工程自動化 - 思路整理

畫渣程式猿發表於2017-08-20

4 月份參加 2017@Swift 大會的時候有幸聽到了 @zesming 大佬關於美團元件化的 Topic,有一張圖印象特別深刻。

來自 @zesming 大佬
來自 @zesming 大佬

後來跟 @zesming 大佬溝通怎麼去整理元件自動構建釋出思路的時候他也跟我提到了這張圖。所以我準備圍繞這張圖來整理一下 iOS 工程自動化的思路。

基礎知識

首先,我們需要掌握一些自動構建釋出的基礎知識,主要包含如下幾個方面。

GitFlow - 規範 git 操作流程

GitFlow 是由 Vincent Driessen 提出的一個 Git 操作流程標準。包含如下幾個關鍵分支:

名稱 說明
master 主分支
develop 主開發分支,包含確定即將釋出的程式碼
feature 新功能分支,一般一個新功能對應一個分支,對於功能的拆分需要比較合理,以避免一些後面不必要的程式碼衝突
release 釋出分支,釋出時候用的分支,一般測試時候發現的 bug 在這個分支進行修復
hotfix 熱修復分支,緊急修 bug 的時候用

GitFlow 的優勢有如下幾點:

  • 並行開發:GitFlow 可以很方便的實現並行開發:每個新功能都會建立一個新的 feature 分支,從而和已經完成的功能隔離開來,而且只有在新功能完成開發的情況下,其對應的 feature 分支才會合併到主開發分支上(也就是我們經常說的 develop 分支)。另外,如果你正在開發某個功能,同時又有一個新的功能需要開發,你只需要提交當前 feature 的程式碼,然後建立另外一個 feature 分支並完成新功能開發。然後再切回之前的 feature 分支即可繼續完成之前功能的開發。
  • 協作開發:GitFlow 還支援多人協同開發,因為每個 feature 分支上改動的程式碼都只是為了讓某個新的 feature 可以獨立執行。同時我們也很容易知道每個人都在幹啥。
  • 釋出階段:當一個新 feature 開發完成的時候,它會被合併到 develop 分支,這個分支主要用來暫時儲存那些還沒有釋出的內容,所以如果需要再開發新的 feature,我們只需要從 develop 分支建立新分支,即可包含所有已經完成的 feature
  • 支援緊急修復:GitFlow 還包含了 hotfix 分支。這種型別的分支是從某個已經發布的 tag 上建立出來並做一個緊急的修復,而且這個緊急修復隻影響這個已經發布的 tag,而不會影響到你正在開發的新 feature

Glow flow 是如何工作的

新功能都是在 feature 分支上進行開發

feature 分支都是從 develop 分支建立,完成後再合併到 develop 分支上,等待發布。

當需要釋出時,我們從 develop 分支建立一個 release 分支

然後這個 release 分支會發布到測試環境進行測試,如果發現問題就在這個分支直接進行修復。在所有問題修復之前,我們會不停的重複釋出->測試->修復->重新發布->重新測試這個流程。

釋出結束後,這個 release 分支會合併到 developmaster 分支,從而保證不會有程式碼丟失。

master 分支只跟蹤已經發布的程式碼,合併到 master 上的 commit 只能來自 release 分支和 hotfix 分支。

hotfix 分支的作用是緊急修復一些 Bug。

它們都是從 master 分支上的某個 tag 建立,修復結束後再合併到 developmaster 分支上。

GitFlow 工具

如果要在專案中引入 GitFlow,推薦使用 SourceTree 來做客戶端工具,它包含了所有 GitFlow 的流程,視覺化操作,很方便。

gitignore - 幹掉那些干擾程式碼

為什麼提到 gitignore,審過 PR(Pull request) 或者 MR(Merge request)的同學應該深有體會,當一個帶著一大堆 Pods 庫程式碼的 PR/MR 襲來的時候,你的內心應該是絕望的……所以我們要了解下怎麼把一些非模組相關的程式碼 ignore 掉。

建立專案倉庫中的 .gitignore

如果你在專案倉庫內建立一個 .gitignore 檔案,Git 會根據這個檔案來決定哪些檔案和目錄是需要忽略的。

注意:如果你想把一個已經被跟蹤的檔案 ignore 掉,這是時候新增的規則並不會對這個檔案產生作用,你需要先用下面的指令把這個檔案設定為不跟蹤:

git rm --cached FILENAME複製程式碼

建立全域性的 .gitignore

這個其實沒啥意思,不過還是看一下怎麼設定吧:

git config --global core.excludesfile ~/.gitignore_global複製程式碼

iOSer 需要的 .gitignore

Github 官方模版給出的建議如下:

# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## Build generated
build/
DerivedData/

## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/

## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint

## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build

# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output

# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/複製程式碼

通過這個檔案我們可以看到,忽略的內容主要包含下面幾種:

  • 構建產生的目錄和檔案;
  • 各種臨時配置檔案;
  • Obj-C / Swift 相關的特定檔案;
  • CocoaPods 第三方 Pods 庫目錄;
  • Carthage 構建目錄;
  • fastlane 構建產生的檔案;
  • 程式碼注入工具產生的目錄及檔案。

我們可以根據自身專案的情況來對這些配置進行相應的調整和定製。

githooks - 背後的“男人”

注:本段主要翻譯自 githooks 官方文件。

githooks 簡介

githooks 是指 git 在執行某些特定操作時會觸發的一系列程式,有點類似資料庫中的觸發器,由使用者編寫。預設情況下,這些程式都被放置在 $GIT_DIR/hooks 目錄下,當然,我們也可以通過 git 的環境配置變數 core.hooksPath 來改變這個目錄。

githooks 的型別有很多中,這些 hook 會在各個特定操作時幫你自動完成一些你想要做的操作,比如:在提交程式碼到 develop 分支的時候自動打包。

目前支援的 githooks

git commit 流程

pre-commit

git commit 觸發,可以通過 --no-verify 選項來跳過,這個 hook 不需要引數,在得到提交訊息並開始提交(commit)前被呼叫,如果返回非 0,則會導致 git commit 失敗。

當預設的 pre-commit 鉤子被啟用時,如果它發現檔案尾部有空白行,此次提交就會被終止。

如果進行 git commit 的命令沒有指定一個編輯器來修改提交資訊(commit message),任何的 git commit hook 被呼叫時都會帶上環境變數 GIT_EDITOR=:

prepare-commit-msg

執行 git commit 命令後,在預設提交訊息準備好後但編輯器啟動前,這個 hook 就會被呼叫。

它接受 1 到 3 個引數。第 1 個是包含了提交訊息的檔案的名字。第 2 個是提交訊息的來源,它可以是:

  • message(如果指定了 -m 或者 -F 選項)
  • template(如果指定了 -t 選項,或者在 git config中開啟了 commit.template 選項)
  • merge(如果本次提交是一次合併,或者存在檔案 .git/MERGE_MSG
  • squash(如果存在檔案 .git/SQUASH_MSG
  • commit 並且第 3 個引數是一個提交的 SHA1 值(如果指定了 -c,-C 或者 --amend 選項)

如果返回值不是 0,那麼 git commit 命令就會被中止。

這個 hook 的目的是用來在工作時編輯資訊檔案,並且不會被 --no-verify 選項跳過。一個非 0 值意味著 hook 工作失敗,會終止提交。它不應該用來作為 pre-commit 鉤子的替代。

commit-msg

git commit 觸發,可以通過 --no-verify 選項來跳過,接受 1 個引數,這個引數包含了提交訊息的檔案的名字,如果返回非 0,則會中止 git commit 命令。

這個 hook 可以用來規範提交資訊,比如把資訊格式化成專案定製的標準格式,或者發現提交資訊不符合格式時拒絕這次提交。

post-commit

git commit 觸發,在提交後被呼叫,不能影響 git commit 的結果。

git push 流程

pre-push

git push 執行期間,更新了遠端引用但尚未傳送物件時執行。如果返回非 0,將中止推送過程。它接受遠端分支的名字和位置作為引數,同時從標準輸入中讀取一系列待更新的引用。 你可以在推送開始之前,用它驗證對引用的更新操作。

pre-receive

服務端處理來自客戶端的推送操作時,最先被呼叫的 hook。它從標準輸入獲取一系列被推送的引用。如果它以非 0 值退出,所有的推送內容都不會被接受。你可以用這個 hook 阻止對 non-fast-forward 的更新,或者對該推送所修改的所有引用和檔案進行訪問控制。

update

pre-receive hook 十分類似,不同之處在於它會為每一個準備更新的分支各執行一次。假如推送者同時向多個分支推送內容,pre-receive 只執行一次,相比之下 update 則會為每一個被推送的分支各執行一次。它不會從標準輸入讀取內容,而是接受三個引數:引用的分支名;推送前引用所指向內容的 SHA-1 值;以及使用者準備推送內容的 SHA-1 值。如果這個 hook 以非 0 值退出,只有相應的那一個引用會被拒絕;其餘的依然會被更新。

post-receive

在整個過程完結以後執行,可以用來更新其他系統服務或者通知使用者。它接受與 pre-receive 相同的標準輸入資料。它的用途包括給某個郵件列表發信,通知持續整合伺服器,或者更新缺陷追蹤系統 —— 甚至可以通過分析提交資訊來決定某個問題是否應該被開啟、修改或者關閉。該指令碼無法中止 push 程式,不過客戶端在它結束執行之前將保持連線狀態,所以如果你想做其他操作需謹慎使用它,因為它將耗費你很長的一段時間。

post-update

由遠端倉庫的 git-receive-pack 呼叫,也是就是本地倉庫完成 git push 時。這個指令只能用來做通知,不能改變 git-receive-pack 的結果。

push-to-checkout

由遠端倉庫的 git-receive-pack 呼叫,也是就是本地倉庫完成 git push 時。如果這個 hook 返回非 0 值,則會中斷 git push 操作。

applypatch-msg & pre-applypatch & post-applypatch

git am 觸發,主要用於引入第三方 patch 的時候用。因為專案中暫時沒有這樣的使用場景,所以不做具體研究。

pre-rebase

git rebase 觸發,可以用來防止某個分支被 rebase。這個 hook 接受 1 到 2 個引數。第 1 個引數是上游分支,第 2 個引數是將要執行 rebase 的分支。

post-checkout

git checkout 成功執行後執行。你可以根據你的專案環境用它調整你的工作目錄。包括放入大的二進位制檔案、自動生成文件或進行其他類似這樣的操作。

post-merge

git merge 成功執行後執行。你可以用它恢復 Git 無法跟蹤的工作區資料,比如許可權資料。這個 hook 也可以用來驗證某些在 Git 控制之外的檔案是否存在,這樣你就能在工作區改變時,把這些檔案複製進來。

post-rewrite

這個 hook 被那些會替換提交記錄的命令呼叫,比如 git commit --amendgit rebase(不過不包括 git filter-branch)。 它唯一的引數是觸發重寫的命令名,同時從標準輸入中接受一系列重寫的提交記錄。 這個 hook 的用途很大程度上跟 post-checkoutpost-merge 差不多。

sendemail-validate

這個 hook 由 git send-email 觸發,它接受一個引數:包含 e-mail 接受者郵箱的檔名。如果這個 hook 返回非 0 值,git send-email 就會被中止。

pre-auto-gc

Git 的一些日常操作在執行時,偶爾會呼叫 git gc --auto 進行垃圾回收。這個 hook 會在垃圾回收開始之前被呼叫,可以用它來提醒你現在要回收垃圾了,或者依情形判斷是否要中斷回收。

Cocoapods - 大管家

做為一個第三方庫依賴管理工具,Cocoapods 在模組化開發中扮演了非常核心的角色。關於 Cocoapods 的功能和介紹這裡就不一一陳述了,主要看一下和模組化開發過程中相關的一些東西。

自定義 Cocoapods 模板

我們一般用 pod lib create 這個指令來建立一個模組,其實這個指令還有一個選項:--template-url=URL,用來指定生成庫的模板,官方模板地址是:github.com/CocoaPods/p…,根據這個模板生成的工程目錄結構如下:

MyLib
  ├── .travis.yml
  ├── _Pods.xcproject
  ├── Example
  │   ├── MyLib
  │   ├── MyLib.xcodeproj
  │   ├── MyLib.xcworkspace
  │   ├── Podfile
  │   ├── Podfile.lock
  │   ├── Pods
  │   └── Tests
  ├── LICENSE
  ├── MyLib.podspec
  ├── Pod
  │   ├── Assets
  │   └── Classes
  │     └── RemoveMe.[swift/m]
  └── README.md複製程式碼

也就是說,我們可以 fork 這份官方模版地址,然後定製我們自己的模板(包括主工程和業務模組),增加自定義的功能。比如:

  • 新增所有基礎庫依賴。
  • 新增私有 Cocoapods 倉庫。
  • 新增 .gitignore 檔案。
  • 新增自定義指令碼。

私有 Cocoapods 倉庫

和主工程一樣,模組也是需要做版本管理的,目前來看比較好的方式就是通過 Cocoapods 來發布版本。所以我們需要準備一個自己的私有 Cocoapods 倉庫,其實很簡單,首先在內網 git 上建立一個空倉庫,然後在本地執行一下下面的指令即可:

pod repo add [Spec name] [Git url]複製程式碼

然後就是在釋出庫的時候注意一下,用如下指令即可釋出到私有倉庫內:

pod repo push [Spec name] [Lib name].podspec複製程式碼

Cocoapods 引用第三方庫的幾種方式

使用過 Cocoapods 的童鞋應該都知道,Cocoapods 的引用方式有三種:

方式 例子 說明
版本號引用 pod 'Alamofire', '~> 3.0' 這種方式引用的是已經發布的版本,包含了 >``>=``<``<=``~> 幾種版本限制符號,其中~>符號代表只更新最新的小版本號,比如 ~> 1.0.0 則只會更新到 1.0.x 的最新版本,而不會更新 1.x.0 以上的版本
本地路徑引用 pod 'Alamofire', :path => '~/Documents/Alamofire' 這種方式直接引用本地的程式碼,這種方式下對引用庫的修改仍然會提交到引用庫的 git 上,而不會提交到主工程。
遠端 git 路徑引用 pod 'Alamofire', :git => 'github.com/Alamofire/A…' 這種方式直接引用遠端 git 程式碼,不需要引用的庫進行釋出,而且還支援 :branch =>:tag =>:commit => 三種選項

流程分析

下面就是我對 @zesming 大佬分享的流程圖中各個過程的一些思考。

業務方需求到開發

根據 GitFlow 的規範,新的需求走的是 feature 的流程。這裡我們應該可以開發一些輔助開發的指令碼。

一鍵新建業務模組和主工程 feature 分支

元件化到了一個完整階段的時候,主工程應該是沒有程式碼的,只是一個殼。但是在發展階段的時候,主工程還會包含一些業務程式碼,所以我們在開發某個 feature 的時候,往往是模組內有一些具體業務程式碼,主工程還有一些呼叫程式碼,這個時候就需要在主工程和業務模組都新建同樣的 feature 分支,所以我們在主工程增加一個指令碼。這個指令碼的引數包含:

  • 業務模組名;
  • 業務模組本地路徑;
  • feature 分支名。

執行過程如下:

  1. 為主工程建立 feature 分支;
  2. 進入業務模組所在目錄,為業務模組建立 feature 分支;
  3. 設定主工程 Podfile 中業務模組引用方式為本地路徑引用;
  4. 開啟主工程和業務模組 Example 工程。

一鍵切換主工程開發除錯狀態

一鍵切換主工程開發狀態是指某些業務模組需要依賴主工程來進行除錯的時候(PS:當然,比較理想的狀態是業務模組可以獨立執行,不過一般情況下,理想很美好,現實很殘酷?),需要將 Podfile 中這個模組的引用方式修改為本地路徑的引用方式。這樣主工程程式碼的修改和業務模組程式碼的修改會分別提交到各自的 git 倉庫,從而實現邊除錯邊開發邊提交程式碼。

這個功能考慮用指令碼的方式來實現,放置在主工程的常用指令碼目錄下。引數應該包含:

  • 業務模組名;
  • 業務模組本地路徑;
  • 業務模組 feature 分支名 [可選]。

執行的操作應該包含:

  1. 如果指定了業務模組 feature 分支名,則需要先給業務模組新建分支,否則直接執行第 2 步;
  2. 修改主工程 Podfile 中業務模組的引用方式為本地路徑引用;
  3. pod install
  4. 關閉主工程並重新開啟。

一鍵切換主工程提交狀態

業務模組開發除錯完成之後,需要將主工程恢復到正常的狀態並提交。

這個功能還是用指令碼的方式實現,放置在主工程的常用指令碼目錄下。引數應該包含:

  • 業務模組名;
  • 業務模組遠端 git 地址;
  • 業務模組 feature 分支名。

執行的操作應該包含:

  1. 根據當前業務模組本地路徑,提交業務模組程式碼;
  2. 修改主工程 Podfile 中業務模組的引用方式為遠端 git 引用;
  3. pod install
  4. 關閉主工程並重新開啟。

提交程式碼到業務程式碼倉庫

這個過程我們主要通過 githooks 來做一些一些自動檢測。包括:

  • OCLint
  • 單元測試

釋出元件到內網 Pods

準備階段

這個階段做的事情主要是檢測當前需要釋出的所有分支是否都已經提交 PR / MR 併合併到了 develop 分支。

注:這裡對分支的命名會有一些規則上的要求比。如在分支名內需要帶上當前需要釋出的版本號,從而可以通過這個版本號匹配到當前需要釋出的所有分支。

釋出 -> 測試 -> bug fix -> 再次釋出階段

這裡需要把 GitFlow 和 Cocoapods 的釋出流程結合起來。

考慮到一般需要依賴主工程來進行測試,我們需要在主工程增加一個本地指令碼來輔助釋出,包含的引數如下:

  • 當前版本號;
  • 業務模組本地路徑;
  • 主工程 feature 分支名[可選]。

實現的功能如下:

  1. 如果提供了主工程 feature 分支名,需要先切換主工程分支;否則跳過這一步;
  2. 根據傳入的當前版本號從 develop 分支建立 release 分支。如果已經存在,則跳過這一步;
  3. 將主工程 git 倉庫中 Podfile 引用該模組的方式替換為引用遠端 git 倉庫的 release 分支;
  4. 然後在主工程執行 pod update [模組名] 更新程式碼;
  5. 推送主工程程式碼到遠端 git 倉庫。
  6. 通過 githooks 的方式打包主工程並提交測試;
  7. 測試過程中如果存在問題,則通過一鍵切換主工程開發除錯狀態指令碼直接切換開發狀態並進行 bug fix;
  8. bug fix 之後重新執行第 1 步。

測試完成釋出到內網 Pods 階段

測試完成後就可以釋出業務模組到內網 Pods 了。這裡我們在業務模組工程內準備一個指令碼,引數如下:

  • 當前版本號。
  • 主工程本地路徑。

實現的功能如下:

  1. 首先我們要修改 .podspec 檔案中的版本號為傳入的當前版本號,並提交 push 到 release 分支。
  2. 然後根據 GitFlow 的 release 流程,合併 release 分支到 develop 分支和 master 分支,然後在 master 分支建立對應版本號的 tag 並 push 到遠端 git 倉庫。
  3. 然後就可以釋出業務模組了,釋出完成之後切換到主工程路徑將主工程 Podfile 中對該模組的引用方式修改為版本號引用。

整合元件到主工程

因為之前釋出元件的時候已經將主工程對各業務模組的引用方式修改為版本號引用。所以這個階段我們只需要驗證一下當前主工程引用的是否是各業務模組的最新發布版本即可。引數如下:

  • 主工程依賴的所有業務模組列表

完成的功能如下:

檢測主工程依賴模組的所有業務模組的最新版本是否和 Podfile 中指定的一致,如果不一致,報錯。

釋出主工程

準備階段

這個階段做的事情有兩點:

  1. 檢測主工程當前需要釋出的所有分支是否都已經提交 PR / MR 併合併到了 develop 分支。
  2. 檢測主工程當前引用的所有業務模組是否都為版本號引用。

釋出 -> 測試 -> bug fix -> 再次釋出階段

這個階段可以理解為整合測試階段。同樣是主工程中的一個指令碼。引數如下:

  • 當前版本號。
  1. 根據傳入的當前版本號從 develop 分支建立 release 分支。如果已經存在,則跳過這一步;
  2. 推送主工程程式碼到遠端 git 倉庫;
  3. 通過 githooks 的方式打包主工程並提交測試;
  4. 測試過程中如果存在問題,則通過一鍵切換主工程開發除錯狀態指令碼直接切換開發狀態並進行 bug fix;
  5. bug fix 之後重新執行第 1 步。

測試完成釋出

  1. 根據 GitFlow 的 release 流程,合併 release 分支到 develop 分支和 master 分支,然後在 master 分支建立對應版本號的 tag 並 push 到遠端 git 倉庫;
  2. 通過 githooks 的方式打包主工程併發布。

總結

本文只是一個不成熟的思考,後續實施的過程中可能會對一些細節進行改善,也歡迎大家對我思路中不合理的地方進行指正,我會非常感激。

下一篇的內容應該是業務方需求到開發這個過程中的一些具體的實踐記錄。敬請期待!

參考資料

Introducing GitFlow:datasift.github.io/gitflow/Int…

Using Git / Ignoring files:
help.github.com/articles/ig…

githooks:git-scm.com/docs/githoo…

自定義 Git - Git 鉤子:git-scm.com/book/zh/v2/…

相關文章