CIでのiOSアプリ依存パッケージビルド時間を削減する

f:id:temoki-ht:20210504135039j:plain mohamed HassanによるPixabayからの画像

こんにちは、スタメンでモバイルアプリ開発を担当している @temoki です。

2月に Mobile Act ONLINE #3 というオンライン勉強会に参加し、iOSパッケージマネージャー奮闘記 というテーマで発表しました(詳しくは以下のスライドをご覧ください)。


この発表でお話しした内容の背景にあるのは CIでのiOSアプリビルド時間を短縮したい ということです。CIサービスは実行時間が利用料金に関連しますし、何よりユニットテストやアプリのテスト配信などの待ち時間は極力減らしたいですよね。

弊社のiOSアプリのビルド時間に大きく影響しているのは Firebase iOS SDK などの依存パッケージのビルドです。そこでこの記事では。依存パッケージの導入方法の工夫により、CIでのビルド時間を削減する過程をお話ししようと思います。

この記事の内容は以下の環境で実施した結果です。

  • 開発環境
    • MacBook Pro 2018 / CPU : 2.2GHz 6コアIntel Core i7 / メモリ : 16GB
    • Xcode 12.5
    • Carthage 0.37.0
  • 依存パッケージ
    • Firebase iOS SDK (Analytics, Auth, Firestore, Cloud Messaging など全7種類)
    • 他、10種類のパッケージとそれらが依存するパッケージで計17種類

Swift Package Manager を利用する

iOS 向けのパッケージマネージャーは主に以下の3つから選択することになります。

  • CocoaPods *1
  • Carthage *2
  • Swift Package Manager *3 (以降、SwiftPMと記載)

SwiftPM は唯一の Swift公式パッケージマネージャーですが、他に比べると後発で、iOSアプリ開発で利用できるようになったのも2019年とまだまだ若いツールです。しかし、この2年でツールとしての課題や各パッケージの対応状況が大きく改善されました。現在では第一選択にしても良いレベルになってきていますので、まずは SwiftPM を選択してみます。

弊社が提供しているアプリでは、Firebase iOS SDK *4 に加えて十数種類のパッケージに依存しています。Firebase iOS SDK は2020年8月に SwiftPM 経由でのインストールがベータ版という扱いで提供されるようになりましたし *5、他の依存パッケージもすべて SwiftPM への対応が完了していましたので、すべてのパッケージを SwiftPM で導入することができました。

この状態でiOSシミュレータ向けのビルドを行い、その時間を計測した結果が下表です。やはり依存パッケージに関する処理に多くの時間がかかっていることがわかりましたので、ここから少しずつ工夫して削減していこうと思います。

ビルド処理 時間
🍎 SwiftPM 依存パッケージの解決 3分
🍋 SwiftPM 依存パッケージのビルド
🍊アプリのビルド ※
5分
8.00分

※ Xcode は SwiftPM 依存パッケージやアプリそのもののビルドも並列で行っているようなので、パッケージとアプリのビルドは1つにまとめられています。

SwiftPM でパッケージのソースをキャッシュする

Xcode から SwiftPM を利用する場合、GitHub等からクローンしてきた依存パッケージのソースコードを再利用できるようになっています。CIサービスにはたいていキャッシュする機能が用意されていますので、これを利用して依存パッケージのソースコードをキャッシュします。具体的な方法は以下の Qiita の記事にまとめていますのでご参照ください。

qiita.com

この方法により、依存関係の解決やクローンの処理は初回のみで、以降はキャッシュを利用してその処理を丸ごとスキップできるようになりました!

ビルド処理 時間
🍎 SwiftPM 依存パッケージの解決 (キャッシュなし) 3分
🍋 SwiftPM 依存パッケージのビルド
🍊アプリのビルド
5分
(キャッシュなし) 8分
(キャッシュあり) 5分

できれば依存パッケージをビルドした成果物もキャッシュできると良いのですが、Qiita の記事にも書きましたとおり、現時点ではビルド成果物をキャッシュすることはできません。

Carthage でパッケージのビルド成果物をキャッシュする

パッケージのビルド成果物もキャッシュできるとさらなる時間短縮が見込めます。ビルド成果物をキャッシュするためにはダイナミックリンクできる Binary Framework 形式でのビルドが必要となります。

現時点では Carthage か CocoaPods のプラグイン*6 を使用することで Binary Framework 形式でビルドすることができますが、Apple Silicon を搭載した Mac でも利用できる新しい Binary Framework 形式である XCFramework *7 に対応しているのは Carthage のみです。よって、できる限り Carthage でパッケージを導入するように変更し、ビルドした XCFramework ファイルを CI でキャッシュするように設定します。

Firebase iOS SDK はしばらくの間 Experimental という扱いで Carthage によるインストールに対応していたのですが、XCFramework への対応をきっかけに Carthage での提供は継続しないという宣言がありました(2020年12月)*8。つまり、Firebase iOS SDK は Carthage でインストールすることはできませんので、それ以外のパッケージのみ Carthage に変更しました。

(2021年5月28日 追記) 2021年5月に Firebase iOS SDK の新しいメジャーバージョン 8.0.0 がリリースされました。Carthage が XCFramework 形式に対応されたため、このバージョンから Carthage での提供が再開されたようです。 ただし、8.0.0 では Cloud Firestore が依存する gRPC-C++.xcframework がインストールされない問題 *9 がありますのでご注意ください。

その結果、SwiftPM 依存パッケージのビルド時間が1分ほど減りましたが、これは思っていたほどの効果ではありませんでした。Firebase iOS SDK が他のパッケージに比べて圧倒的に大きく、ビルド時間の大半を占めているということですね。

ビルド処理 時間
🥝 Carthage 依存パッケージの解決/ビルド (初回のみ) 17分
🍎 SwiftPM 依存パッケージの解決/クローン (初回のみ) 2分
🍋 SwiftPM 依存パッケージのビルド
🍊アプリのビルド
4分
(キャッシュなし) 23分
(キャッシュあり) 4分

また、初回のみとはいえ Carthage のビルド時間は非常に多くの時間がかかってしまう(1パッケージあたり平均1分くらい)のも気になります。もちろん、 --platform iOS オプションにより iOS 向けのビルドに限定するなど、最低限のビルドに抑えていてこの状態です。

iOSデバイス用・シミュレータ用など必要なバイナリを全てビルドする必要があることが原因なのでしょうか。せめて複数のパッケージを並列でビルドする機能があれば良いのですが、現時点では対応されていないようです *10

対して SwiftPM は Xcode 10 で導入された New Build System により適切に並列ビルドしてくれるので、Carthage に比べてビルド時間が短くなっています。

Carthage で不要なパッケージのビルドをスキップする

Carthage はパッケージのXcodeプロジェクトに含まれる全ての共有スキームをビルドします。そのため、アプリから使用しないスキームもビルドすることになり、無駄な時間がかかってしまいます。この不要なビルドを次の方法でスキップすることでビルド時間を少し抑えることができます。

qiita.com

今回のケースでは6つの XCFramework のビルドをスキップすることができ、Carthage のビルド時間が6分減りました。

ビルド処理 時間
🥝 Carthage 依存パッケージの解決/ビルド (初回のみ) 11
🍎 SwiftPM 依存パッケージの解決 (初回のみ) 2
🍋 SwiftPM 依存パッケージのビルド
🍊アプリのビルド
4
(キャッシュなし) 17分
(キャッシュあり) 4分

Firebase iOS SDK のビルド済み XCFramework をマニュアルインストールする

さて、最も多くのビルド時間が費やされている Firebase iOS SDK の工夫にとりかかります。Firebase iOS SDK の GitHub リポジトリのリリースページでは、各リリースに対してビルド済みの XCFramework がまとめられた Firebase.zip が添付されています *11

このファイルはバージョン 7.11.0 で 300MB もありますので、アプリのリポジトリに追加するには少し大きすぎます。そこで、このファイルをダウンロード・展開・キャッシュするフローをCI上で行うようにします(curlunzip などのコマンドを組み合わせた簡単なスクリプトで自動化できますね)。

CIサービス上であれば高速なネットワーキングにより30〜60秒程度でダウンロードと展開が終わってしまいますので、初回のキャッシュにかかる時間も最小限で済みます。

この結果、アプリのビルド時間は 1.5 分までに縮まりました!最初は 8 分かかっていたので 80%も削減 できたことになります 🎉

ビルド処理 時間
🥝 Carthage依存パッケージの解決とビルド (初回のみ) 11分
🔥 Firebase iOS SDKのマニュアルインストール (初回のみ) 1分
🍋 SwiftPM 依存パッケージのビルド
🍊 アプリのビルド
1.5分
(キャッシュなし) 13.5分
(キャッシュあり) 1.5分

Carthage のビルド時間が気になる場合は、Firebase iOS SDK 以外は SwiftPM でインストールするように戻しましょう。SwiftPM で導入したパッケージは毎回ビルドする必要がありますが、SwiftPM での並列ビルドが高速なので +0.5分 程度の増加のみでした。開発環境をシンプルに保ちたい場合はこちらの方が良さそうですね。

ビルド処理 時間
🔥 Firebase iOS SDKのマニュアルインストール (キャッシュなし) 1分
🍎 SwiftPM依存パッケージの解決 (キャッシュなし) 2分
🍋 SwiftPM/依存パッケージのビルド
🍊アプリのビルド
2分
(キャッシュなし) 5分
(キャッシュあり) 2分

さいごに

今回は弊社の提供するiOSアプリを例に、依存パッケージのビルド時間を削減していく過程をお伝えしました。依存パッケージやCI環境によって最適な方法は異なると思いますが、工夫のポイントなどで参考になれば幸いです。

最後になりますが、スタメンでは自社プロダクトの開発する仲間を募集しています。興味を持ってくれた方は、ぜひ下記の採用サイトをご覧ください。


*1:CocoaPods : CocoaPods is a dependency manager for Swift and Objective-C Cocoa projects.

*2:Carthage : Carthage is intended to be the simplest way to add frameworks to your Cocoa application.

*3:Swift Package Manager : The Swift Package Manager is a tool for managing the distribution of Swift code.

*4:https://firebase.google.com/docs/ios/setup?hl=ja

*5:https://github.com/firebase/firebase-ios-sdk/blob/master/SwiftPackageManager.md

*6:CocoaPods BinaryCocoaPods Rome : どちらのプラグインも XCFramework への対応は進んでいません。

*7:Apple Developer / Distributing Binary Frameworks as Swift Packages

*8:firebase-ios-sdk / WARNING: Carthage May Be Discontinued

*9:Firestore Carthage installation is missing gRPC-C++ in 8.0.0

*10:Carthage / Parallel building #1104

*11:https://github.com/firebase/firebase-ios-sdk/releases : Xcodeプロジェクトへの組み込みは、ZIPファイル内にある README.md に記載されています。