iOS 打包系統構建加速

目標

iOS 單包構建加速、支持多包並行打包

基礎知識

CI、CD 在稍微有點規模的公司內部都會內建一套自己的系統。目前主流的是在 Jenkins 的基礎上進行的打包系統。公司只有1個 App 的情況下一臺打包機就夠了,但是有多個 SDK、App 那肯定不夠的,各個業務線都需要測試、上架等等,任務太多了,一臺機器別人要等到花兒謝了…

分佈式構建系統可解決上述問題,即一個 master 爲中心,多個 slave 來進行具體的構建操作。多臺執行機來進行任務的構建以及自動化腳本的執行。Jenkins 具備分佈式特性,是 Master/Slave 模式(主從模式,將設備分爲主設備和從設備,主設備負責分配工作並整合結果,或作爲指令的來源;從設備負責完成任務,從設備一般只和主設備通信)。這個模式有2個好處:

  • 能夠有效分擔主節點的壓力,加快構建速度
  • 能夠指定特定的任務在特定的主機上進行

背景

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hqCAuK4K-1585662119830)(https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/2019-12-16-candle.png)]

  • 描述現狀

    我們公司的 WAES 平臺下子平臺 candle 是專門用來打包構建的,可以打包 iOS SDK、iOS App、Android SDK、Android App、React Native 包、H5、Node 包、React 包等等。iOS 端將 SDK、App 通過 candle 打包平臺進行任務創建、排隊、打包,根據任務的特點和語言去調度合適的打包機器進行打包。 現狀是整體速度覺得較慢,還有加速空間。

  • 問題原因

    1. iOS 打包目前都是在使用舊的打包構建系統,所以在單包構建方面會慢一些;
    2. 在 pod install 這一步,打包機使用的 1.3.1 版本的 cocoapods 版本在進行依賴分析,它本質上是操縱打包機上全局的 git 目錄,由於本質如此所以沒辦法多包並行打包。
  • 風險預警

    1. cocoapods 升級到最新版本,目前的 cocoapods 的相關的 ruby 腳本可能會有問題,沒辦法良好運行。
    2. 開啓 Xcode 的新構建系統,可能會造成現有工程報錯,沒辦法編譯成功,需要改動。這些改動可能不只是主工程,也許是各個 SDK 自身的修改,所以會比較零散。

改造

單包加速

一些背景:
  1. 打包機暫時不支持各個依賴的 SDK 以靜態庫的方式引入。

    原因是目前 APM 監控系統不支持。因爲多個靜態庫則會生成多個 DYSM 文件,這樣子 APM 定位 Crash、ANR 等都會生成多個 DYSM 文件的信息,配套的後端在符號化處理的時候只支持一個 DYSM 的模式,所以在如此背景下打包機不支持靜態庫。

  2. 看到博客說可以通過 generate_multiple_pod_projectsdisable_input_output_paths 來加速構建速度,這些在本地開發過程中是可以提高構建速度的。但是在打包機這種環境下是不太適用的。因爲 iOS 在打包機環境下都會執行 pod install 的過程.

    install! 'cocoapods', :generate_multiple_pod_projects => true, :incremental_installation => true, :disable_input_output_paths => true
    
    • generate_multiple_pod_projects
      生成多個 XcodeProj。在 1.7.0 之前,cocoapods 只生成一個 Pod.xcodeproj,隨着 Library 增多,文件越來越大,解析時間越來越長,在 1.7.0 之後每個 Library 都允許生成單獨的 Project,提高項目的編譯時間。 默認關閉
    • incremental_installation
      增量安裝,每次執行 pod install 都會生成整個 workspace,現在支持只更新的 Library 編譯。節省時間
  3. 另外網上的部分優化提速手段也不太適合,因爲這些手段基本上只是會加快一些速度,但是不可能把一個項目的構建速度提升明顯,所以這次的方案主要是單包開啓 New Build System 和支持多包並行能力。

理論基礎

本質上就是開啓 New Build System,蘋果在 WWDC 2017 中描述新構建系統的有點爲:降低構建開銷,尤其可以降低大型項目的構建開銷。但是在新構建系統下現有的工程會報錯。經過查看報錯信息,基本都是在資源方面的錯誤(圖片等)和偶爾一些 SDK 不規範造成的問題。

蘋果從 Xcode 9 開始推出了新構建系統(New Build System),並在 Xcode 10 使用其爲默認構建系統來替代舊構建系統(Legacy Build System)。採用新構建系統能夠減少構建時間。

簡要介紹一下原理,對於舊構建系統,當我們構建一個程序的時候,會明確所需要構建的所有 Target Dependencies、Link Binary With Libraries,這些 Target 之間的依賴關係,以及這些 Target 構建的順序。採用順序會造成多處理器系統資源的浪費,從而表現爲編譯時間的浪費,解決這個問題的方式就是採用並行編譯,這也是新構建系統優化的核心思想。詳細瞭解新構建系統,探究 Xcode New Build System 對於構建速度的提升。

測試實驗

注意: 報錯提示找不到 coderay. 可以運行 sudo gem install coderay 解決該問題。

本地 cocoapods 版本爲 1.4.0,打包機環境爲 1.3.1,所以方案評估有問題。這幾天花時間做了對比實驗,數據如下

  1. New Build System 是否可以讓單包構建變快?
    cocoapods 模擬打包機環境 1.3.1。在 Legacy Build System 和 New Build System 下運行項目。
    1.3.1 不能開啓 New Build System。報錯信息: New Build System Multiple commands produce script phase “[CP] Copy Pods Resources”
    1.3.1 Legacy Build System 構建時間爲 335.4s.
    所以嘗試升級 cocoapods 繼續做對比實驗

  2. cocoapods 小版本升級到 1.4.0 在 Legacy Build System 和 New Build System 下運行項目(爲什麼選擇升級到 1.4.0? cocoapods 小版本升級則改動較小,業務線可以快速享受到 New Build System 改動帶來的收益)
    New Build System: 383.5s
    Legacy Build System: 302.9s

  3. 升級 cocoapods 到 1.8.0,查看在 New Build System 和 Legacy Build System 下的構建時間
    cocoapods 升級到 1.8.0 會報錯,修改錯誤後運行對比。
    New Build System: 324.4s
    Legacy Build System: 262.2s

實驗數據如下:

App 構建系統 Cocoapods 版本 Build 結果 編譯時間
**App New Build System 1.3.1 失敗 ~
**App Legacy Build System 1.3.1 成功 335.4s
**App New Build System 1.4.0 失敗 383.5s
**App Legacy Build System 1.4.0 成功 302.9s
**App New Build System 1.8.4 成功 324.4s
**App Legacy Build System 1.8.4 成功 262.2s

結論:從實驗數據來看, New Build System 並不能單包加速。所以 New Build System 不做了。構建加速是升級cocoapods 1.8.4 帶來的,並不是 new build system 帶來的。後續計劃分2步:

  1. 升級 cocoapods 到 1.8.4,可以體驗到單包構建加速的效果。
  2. 自建 CDN。

拿自己的電腦部署腳本,當作本地打包機;拿**App App 打包,指定打包機爲自己的電腦

App Cocoapods 版本 編譯時間
**App 1.3.1 8m37s
**App 1.8.4 7min47s

開啓 New Build System 帶來的改動

  1. SDK 中圖片是通過 resource 的方式管理的,cocoapods 1.8.4 會將它打包到 Assets.car 和 App 主工程圖片打包的結果一致,導致 Xcode 主工程報錯,大體意思是說工程包含多個 Assets.car. 原因在於 SDK 通過 resource 管理圖片,打出包所以可以使用 resource_bundles 的形式管理

    • 涉及到的 SDK:TrinityConfiguration(公共 SDK)
    • 改造點:
      注意:改變 SDK 內部修改 xcassets 文件名是無用的,Xcode 編譯後查看包內容,結果還是 Assests.car
      圖片使用方式改變。由之前的 resources 方式改爲 resource_bundles 的形式。這樣做有2個優點:解決了圖片資源打包後造成 Assets.car 衝突的問題;resource_bundles 還可以解決圖片訪問速度的優化。
  2. 圖片資源重複
    **App工程中有些圖片和 理財的 SDK SdkFinanceHome 裏面的圖片資源重名,但是內容卻不一致,需要協商改動。

    • 涉及到的 SDK:SdkFinanceHome (理財業務線 SDK)
    • 改造點:
      有2張圖片在 SdkFinanceHome SDK 內重複出現2次(形狀、大小一致)。App 也存在同名的圖片,圖形一致、尺寸大小不一致。所以需要**App業務線開發者確認,保留什麼圖片或者資源重命名。建議圖片資源也用 resource_bundles 的形式管理
      s.resource_bundles = {
      'SdkFinanceHome' => ['***/Assets/*.xcassets']
      }
      

升級 1.8.4 帶來的改動點:

  1. 部分 SDK 的頭文件引用方式有問題
    • 涉及到的 SDK:SdkFundWax

    • 改造點:將 FCH5AuthRouter.m 文件中關於 NativeQS 中頭文件的引入方式改變下。#import <NQSParser.h> 改爲 #import <NativeQS/NativeQS.h>,或者改爲 #import "NQSParser.h"

       "dependencies": {
          "NativeQS": "~> 1.0"
        },
      

      注意:
      因爲打包機目前是源碼引入編譯成 .a 文件。如果是以 framework 的形式,則必須以依賴描述的方式進行調整。

      新版本 cocoapods 中:

      • cocoapods 在 SDK 裏面引用別的 SDK 如果 podspec 裏面存在 dependencies 描述,則可以使用 #import <NativeQS/NativeQS.h> 或者 #import "NativeQS.h";如果不存在 dependencies 描述,則需要使用 #import <NativeQS/NativeQS.h>
      • 在主工程中引用 SDK 的頭文件,使用 #import <NativeQS/NativeQS.h>#import "NativeQS.h" 都可以

      舊版本 cocoapods 中:

      • SDK、App 主工程都可以使用 #import <NativeQS/NativeQS.h>#import "NativeQS.h"#import <NativeQS.h>
  2. 部分 SDK 的使用了未在 podspec 文件中聲明的依賴,在新版本 cocoapods 下會報錯(某些 SDK 由於歷史原因造成新版本丟失依賴描述)
    • 涉及到的 SDK:CMRCTToast

    • 改造點:
      問題基本定位是在於, App 主工程引用的 SdkBbs2 SDK 依賴了 SdkBbs2 版本(該版本的依賴描述爲 CMRCTToast (~> 0.1) )
      歷史原因: 早期在做 RN SDK 封裝的時候在第一個版本的時候只有某個版本的 React Native 庫,所以在 0.1.0 的時候依賴的描述可以看到如下的代碼

      s.dependency 'React/Core', '0.41.2'
      s.dependency 'React/RCTNetwork', '0.41.2'
      s.dependency 'React/RCTImage', '0.41.2'
      s.dependency 'React/RCTText', '0.41.2'
      s.dependency 'React/RCTWebSocket', '0.41.2'
      s.dependency 'React/RCTAnimation', '0.41.2'
      

      隨着版本的不斷迭代,在第二個版本 0.1.1 的時候可以看到下面的描述. 可以看到對 RN 的描述不存在了,因爲當時的代碼對 RN 的2個版本都做了兼容,所以 App 主工程肯定是有 RN 的庫,所以索性就不在單獨描述,直接隨着 App 依賴的 RN 庫而使用。之後的版本也是如此。

      s.dependency 'CMDevice', '~> 0.1'
      

      所以, 將 CMRCTToast.podspec 中的依賴修改掉。需要兼容不同 RN SDK 的版本。

  3. 部分 pod 的 hook 腳本會失敗。
    • 涉及到的 SDK:無
    • 改造點:
      TrinityParams.rb 類方法 generate_mods 會報錯。邏輯是通過遍歷每個 pod_target,獲取到 PBXNativeTarget,然後訪問 source_build_phase 屬性去遍歷內部的每個文件,判斷是否是 properties.yml
      1. 官方文檔地址.
      2. 公共組做的庫,Android 和 iOS 都是對應的,但是 SDK 的名字不一定嚴格一致。但是通跳後臺是配置的時候不可能設置多個名字,所以設置了一個通用的名字,然後 iOS SDK 和 Android SDK 各自用一個描述文件將本地的 SDK 和下發需要命中的 SDK 名字做一個對映射關係。iOS 端用 properties.yml來描述
        cocoapods 新版本里面每個 pod_target 沒有 native_target 屬性,也就是沒辦法獲取到 PBXNativeTarget。
        感覺之前的腳本寫法有問題,內部基既有 file_accessors,也有 pod_target.native_target 的形式繼續訪問 yml 文件。所以升級 cocoapods 1.8.4 之後修改腳本直接改爲用 file_accessors 尋找 yml

多包加速

目前不能開啓多包並行的瓶頸在於打包機操作的是本地下載下來的 .cocoapods 文件夾,所以當一個項目操作的時候其他項目沒辦法操作。

CDN 提供了通過網絡接口處理依賴的能力,通過網絡去操作文件,所以是可以多包並行打包的。

但是由於以下2個原因,我們需要自建CDN的能力:

  1. 我們的依賴存在的位置有2個,1個是私有源、1個是官方源。目前使用官方CDN就是官方源,因爲我們要並行打包,所以私有源也需要 CDN 化。不然難以免於多個項目進行文件的讀寫鎖操作問題。
  2. 但是CDN跟網絡的狀態有關,依據所處位置附近的服務器有關係,嚴重依賴於外界因素,不可控。所以想擁有快速穩定的CDN 查詢能力就需要自建CDN 了。

另外一個可預期的點就是自建了CDN ,wax SDK 發佈的相關邏輯也需要修改。

根據 Cocoapods 的 changeLog 知道 CDN 的實現是藉助 Netlify 實現的。所以接下去的研究方向就是如何利用 Netlify 自建 CDN。

Directory Listing Denied

It was obvious to many that the spec repo should be put behind a CDN, but there were several constraints:

  1. It had to be a free CDN, as the project is free and open-source.
  2. It had to allow some way of obtaining directory listings, for retrieving versions of pods.
  3. It had to auto-update from GitHub as the source of truth.

The first implementation was a shell script, polling GitHub and piping find into ls into index files. This ran on a machine that was not open or free and therefore could not be the true solution. Nevertheless, this auto-updated repo was put behind a jsDelivr CDN and the client interfacing with it was released in 1.7.0 labeled “highly experimental”.

Final Lap with Netlify

The final version of the CDN for CocoaPods/Specs was implemented on Netlify, a static site hosting service supporting flexible site generation. This solution ticked all the boxes: a generous open-source plan, fast CDN and continuous deployment from GitHub.

Upon each commit, Netlify runs a specialized script which generates a per-shard index for all the pods and versions in the repo. If you’ve ever noticed that the directory structure for our Podspecs repo was strange, this is what we call sharding. An example of a shard index can be found at https://cdn.cocoapods.org/all_pods_versions_2_2_2.txt. This would correspond to ~/.cocoapods/repos/master/Specs/2/2/2/ locally.

Additionally, we create an all_pods.txt file which contains a list of all pods.

Finally, any other request made is redirected to GitHub’s CDN.

接入方式

考慮到業務線 App 升級是分開的,不可能同步進行,所以需要考慮到接入計劃。

  • 能否提供 wax 項目指定到特定環境打包機的能力(該打包機升級了 cocoapods 版本)
  • 假如沒有上述能力,則考慮其他方式支持業務線自定義打包所需的 cocoapods 版本
    • 將 2個版本的 cocoapods 做成2個 Bundle 包,讀取 wax 工程配置,指定某個 Bundle
    • 假如打包機由於某些原因沒辦法升級 cocoapods 版本,但是某個 wax 項目又需要新版的 cocoapods 進行打包,則需要則代碼上傳的時候提交 Pods 文件夾。這樣在打包機上面不需要執行 install 的操作,將本地的 Pods 目錄上傳上來,全部使用本地的一套。

參考資料

1. cocoapods changeLog

2. 版本清單

3.探究Xcode New Build System對於構建速度的提升

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