iOS 組件化-混編下的二進制方案

背景: 項目採用 Target-Action + CocoaPods 進行組件化, 去年由 Objective-C 轉向 Swift, 全部新的組件所有使用 Swift 編寫, 主幹項目由 Objective-C 和 Swift 混編. 在體驗到面向值和協議編程的便利的同時, 也深深被 Swift 編譯速度所困擾. 在對 Swift 編譯速度優化後, 效果仍是不夠理想, 因此爲了提升主幹項目打包提測的速度, 決定將組件二進制化.html


關於 Cocoapods 私有庫的建立就不說了, 官方文檔裏也都有用法.git

因爲項目中的業務組件中也會擁有資源文件, 且以前 Swift 不支持靜態庫, 因此採用動態庫 framework 的方式.github

更新: Xcode9 beta 4 和 CocoaPods 1.5 已經支持 Swift 靜態庫.shell

其實就是使用 CocoaPods 對源文件和 .framework 進行管理, 使用 pod installpod update 命令來拉取和切換資源文件.編程

這裏主要記錄一下二進制組件的建立和使用.podspec 配置.swift

其中涉及到的工具備:vim

  • xcodebuild
  • Command Line Tool
  • Xcode
  • CocoaPods

一. 自動建立 framework

使用 Xcode 進行編譯時, 默認生成的 .framework 文件會存放在 Xcode 文件夾下xcode

framework_in_finder

咱們能夠將編譯後的 .framework 移動到文件夾內進行版本管理, 可是若是每次手動拖動到項目文件夾中, 難免有些累贅. 且模擬器和真機也須要不一樣 architecture, 因此咱們能夠編寫一個 build.sh 來自動生成及合併 exec 可執行文件, 並將生成的 .framework 保存到項目目錄內. 其中主要的命令是:ruby

# 生成 iphoneos 和 iphonesimulator 兩種可執行文件
$ xcodebuild -workspace ${WORKSPACE_NAME}".xcworkspace" -configuration "${CONFIG}" -scheme ${SCHEME_NAME} SYMROOT=$(PWD)/build -sdk iphoneos clean build
$ xcodebuild -workspace ${WORKSPACE_NAME}".xcworkspace" -configuration "${CONFIG}" -scheme ${SCHEME_NAME} SYMROOT=$(PWD)/build -sdk iphonesimulator clean build
# 合併爲一個可執行文件
$ lipo -create "${DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${DES_DIR}/${FMK_NAME}"
複製代碼

build.sh 接受兩個參數, 分別是項目名稱和構建配置, 將腳本置於 .workspace 同級目錄下, 如如下命令將輸出 Debug 配置的 BAPurchase.framework:bash

./build.sh BAPurchase Debug
複製代碼

Objective-C framework 中, 使用 lipo 合成 iphoneos 和 iphonesimulator 可執行文件後, .framework 便可正常工做, 不過在合成 Swift framework 後, 使用 .framework 會出現錯誤:

'SomeClass' is unavailable: cannot find Swift declaration for this class

這是由於 Swift framework 內包含有 .swiftmodule 文件, 其定義了 framework 所支持的 architecture, 因此對於 Swift framework, 咱們除了將 .exec 文件合併外, 還須要將 .framework/Module/.swiftmodule 文件夾內的全部描述文件移動到一塊兒:

cp -R "${SIMULATOR_DIR}/Modules/${FMK_NAME}.swiftmodule/." "${DES_DIR}/Modules/${FMK_NAME}.swiftmodule/"
複製代碼

swift_framework_module

腳本完成後, 接下來使用 Xcode 添加一個 Aggregate Target 用來單獨調用腳本, 同時實現工程參數的自動填充.

File->New->Target->cross-platform->Aggregate, 新建一個 Aggregate Target :

create_aggregate

並在 Build Phases->New Run Script Phase 內添加新的 script, 並輸入如下命令用於調用 build.sh:

${SRCROOT}/build.sh ${PROJECT_NAME} ${CONFIGURATION}
複製代碼

build_phases

上面的指令等價於下面兩條命令, 省卻了配置信息的輸入 :

$ cd BAPurchaseDirectory
$ ./build.sh BAPurchase Debug #Release, Debug, InHouse, AdHoc
複製代碼

使用 Aggregate Target 的一大好處是不用每次都 cd 到目錄去運行腳本, 只須要**切換到 Aggregate Target , 確認 Edit Scheme -> Run->Build Configuration , 執行 build **便可.

二 .podspec 配置

.framework 由 shell 生成後位於工程目錄下, 和源文件一塊兒存放, 咱們還須要另外配置 .podspec 實現選擇性導入文件, 介紹兩種方式:

  1. 使用條件語句
if ENV[ 'f' ]
      s.vendored_framework = 'BAPurchase/Frameworks/BAPurchase.framework'
else
      s.source_files = 'BAPurchase/Classes/**/*'
      s.resource_bundles = {
          'Resources' => 'BAPurchase/Assets/*/**'
      }
end
複製代碼

導入時使用如下指令切換到 .framework:

$ pod install
$ f=1 pod update BAPurchase
複製代碼

但這種方式有兩個問題:

  • pod update BAPurchase 的時候會將全部其餘的 pod 也更新, 即若是有多個私有庫都配置了 framework 和源文件導入, 則咱們對其中一個使用 pod update 會致使兩個都切換到 framework 或源文件.
  • 不能在 pod install 時添加參數, 只能 pod install 源文件, 以後再次執行 f=1 pod update BAPurchase 指令切換到動態庫.
  1. 使用 subspec
s.subspec 'Framework' do |sf|
      sf.s.vendored_framework = 'BAPurchase/Frameworks/BAPurchase.framework'
  end
s.subspec 'Core' do |sc|
      sc.source_files = 'BAPurchase/Classes/**/*'
      sc.resource_bundles = {
          'Resources' => 'BAPurchase/Assets/*/**'
      }
end
s.default_subspecs = 'Framework'
複製代碼

以上配置定義了兩個導入方式: 源碼和 .framework, 默認導入方式爲 .framework. 若是須要進行源碼調試, 則能夠修改 podfile 爲 pod BAPurchase/Core, 而後執行如下命令:

$ vim podfile
# pod 'BAPurchase/Core'
$ pod update BAPurchase --no-repo-update
複製代碼

subspec 的好處顯而易見:

  • 能夠在 podfile 內直接指定導入方式爲源碼或 .framework.
  • pod update 時作到互不干擾, 只更新指定的私有庫.

與條件判斷相比, 也有缺點:

  • 須要額外去修改 podfile.

三. Debug or Release

業務組件內頗有可能會包含有環境變量, 最典型的是 Objective-C 中使用預處理宏 #if DEBUG 來獲取測試域名或生產域名, 可是二進制組件是已經編譯完成的, 有可能不能匹配主項目的環境配置. 能夠用兩種方式解決:

  1. subspec

    使用 CocoaPods 的 subspec 能夠完成該配置, 此時組件提供三個subspec:

    • 源碼
    • Debug framework
    • Release framework

    平時默認導入 Debug framework, 在須要調試時切換到源碼, 在線上測試和提審時使用 Release framework. 這樣的優缺點很明顯:

    • 容易實現
    • 可是 pod 須要更頻繁的切換, 在迭代末期, 測試環境和線上環境同時測試時, 操做麻煩且易出錯
    • 在組件提交時, 須要分別對 Debug 和 Release Framework 進行編譯和上傳, 增長工做量和時間消耗.
  2. 組件環境變量

    Debug 和 Release 除了編譯器優化的區別外, 影響最大的就是使用 Preprocessor Macros(Objective-C) 和 Active Compilation Conditions(Swift). 咱們能夠把環境變量的肯定延遲到連接時: 在組件內定義一個環境變量, 全部的域名根據環境變量對應返回, 這個變量由主項目傳遞, 詳情參考示例.

    • 須要在代碼中傳遞環境變量
    • 只須要編譯一個 Release framework 便可.
    • 主項目中無需頻繁切換 framework

我更傾向於使用組件環境變量, 避免在組件中使用 Preprocessor Macros(Objective-C) 和 Active Compilation Conditions(Swift).

另外, 對於項目中大量依賴的三方庫, 也能夠將他們製做成私有 framework, 減小編譯時間, 在須要源碼調試時, 切換成源代碼便可. 也能夠經過 CocoaPods 插件的方式, 在 pod installpod update 後, 將三方代碼直接編譯成 framework 進行集成.

Troubleshooting Tips

  1. Error: Unknown class _SomeModuleSomeCell in Interface Builder file:

    這是因爲組件中的 Xib 有對應的 class, xib 加載後會去將 outlet 賦值到對應類實例, 而類和 xib 不在同一 bundle 內形成錯誤. 因此須要在 xib 的 Identity Inspector->Custom Class->Module 指定類所屬模塊.

class_module

  1. Error: 'ASwiftFrameworkClass' is unavailable: cannot find Swift declaration for this class

    Swift framework 進行多 architecture 合併時, 除了 exec 可執行文件外, 還須要將 .framework/Modules 文件夾內的描述文件一併合併, 不然編譯時會提示錯誤.

  2. Error: Module 'BAPurchase' not found

    在 Objective-C 源項目中導入 Swift framework 後, 會出現此錯誤, 須要在 Objective-C Target -> Build Settings 中, 設置 alwaysEmbedSwiftStandardLibraries = YES

相關文章
相關標籤/搜索