🛠️
Xcode のインクリメンタルビルド(差分ビルド)でキャッシュなしで再コンパイルされるファイルを確認
きっかけ
- マルチモジュールとインクリメンタルビルドの関係を確認するために検証してみた。
確認方法
- Xcode 内の Build からだとコンパイル時にキャッシュが使われているかをフィルタリングできなそうだったため、
xcodebuildを使用。- 自信もソースもないがキャッシュが効いていない場合のコンパイルは
SwiftCompile normal arm64 Compilingがついてそうだったのでそこで判定している。もしもこれが間違っていたら前提から間違っているので詳しい方ご指摘ください🙏
- 自信もソースもないがキャッシュが効いていない場合のコンパイルは
- Compilation Mode, Optimization Mode はデフォルト設定のまま。
xcodebuild \
-project App.xcodeproj \
-scheme App \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 14 Pro,OS=17.0' \
-configuration Debug \
build > >(grep "SwiftCompile normal arm64 Compiling") 2>&1
検証
対象コード
- 同一モジュールで別ファイルに定義した
Viewと別モジュールに定義したViewを用意してどのファイルがコンパイルされたかを確認する。
// ExampleApp.swift
import SwiftUI
import OtherModule
struct ExampleApp: App {
var body: some Scene {
WindowGroup {
VStack {
View1(text: "1") // ExampleApp と同一モジュール内の View
View2(text: "1") // 同上
OtherModuleView("text: 1") // 別モジュールの OtherModule 内の View
}
}
}
}
検証1. ExampleApp.swift 内を変更
struct ExampleApp: App {
var body: some Scene {
WindowGroup {
VStack {
- View1(text: "1")
+ View1(text: "2")
View2(text: "1")
OtherModuleView("text: 1")
}
}
}
}
コンパイルされたファイル
- ExampleApp.swift
確認できたこと
- 変更したファイルのみがコンパイルされた。
検証2. View1.swift 内を変更
-
ExampleApp.swiftではないので差分は割愛
コンパイルされたファイル
- View1.swift
確認できたこと
- 変更した
View1.swiftのみがコンパイルされ、View1を参照しているExampleApp.swiftはコンパイル対象外となった。
検証3. OtherModuleView.swift 内を変更
-
ExampleApp.swiftではないので差分は割愛
コンパイルされたファイル
- OtherModuleView.swift
確認できたこと
- 別モジュールであっても検証2と同様の結果で、
OtherModuleView.swiftのみがコンパイルされ、OtherModuleViewを参照しているExampleApp.swiftはコンパイル対象外となった。
検証4. View1.init(_:) の引数名を変更
struct ExampleApp: App {
var body: some Scene {
WindowGroup {
VStack {
- View1(text: "2")
+ View1(text2: "2")
View2(text: "1")
OtherModuleView("text: 1")
}
}
}
}
コンパイルされたファイル
- View1.swift
- ExampleApp.swift
確認できたこと
-
View1.init(_:)を変更するということは呼び出し元のExampleAppl.swiftも編集しているのでその両方がコンパイルされた。
検証5. OtherModuleView.init(_:) の引数名を変更
struct ExampleApp: App {
var body: some Scene {
WindowGroup {
VStack {
View1(text2: "2")
View2(text: "1")
- OtherModuleView("text: 1")
+ OtherModuleView("text2: 1")
}
}
}
}
コンパイルされたファイル
- OtherModuleView.swift
- ExampleApp.swift
確認できたこと
- 別モジュールであっても検証4と同様の結果で、呼び出し元の
ExampleApp.swiftも編集しているのでその両方がコンパイルされた。
検証6. コメント行を追加
struct ExampleApp: App {
var body: some Scene {
WindowGroup {
VStack {
+ // コメント
View1(text: "2")
View1(text2: "2")
View2(text: "1")
OtherModuleView("text: 1")
}
}
}
}
コンパイルされたファイル
- ExampleApp.swift
確認できたこと
- コメント行であっても変更されたファイルがコンパイルされた。
- 理由はおそらく、コメントはコードに影響が出ないとしても、影響が出ないかどうかはコンパイルしないとわからないためコンパイルされる。
検証7. View1.swift に新しい View を定義
-
ExampleApp.swiftではないので差分は割愛
コンパイルされたファイル
- View1.swift
- View2.swift
- ExampleApp.swift
確認できたこと
- 新しい定義を追加するとそのファイルと依存関係があるすべてのファイルがコンパイルされた。
- これはメソッドやプロパティの追加でも同様の挙動だった。
-
View2.swiftは直接View1.swiftを参照していなくExampleApp.swiftが参照してるだけでもコンパイル対象となった。- ただし、
ExampleApp.swiftが参照していないView3.swiftはコンパイル対象外だったため、フルビルドがされるというわけではない。
- ただし、
-
OtherModuleViewは別モジュールなため、ExampleApp.swiftが参照しててもコンパイル対象外だった。
検証8. OtherModuleView.swift に新しい View を定義
-
ExampleApp.swiftではないので差分は割愛
コンパイルされたファイル
- OtherModuleView.swift
- ExampleApp.swift
確認できたこと
- 検証7 の結果と同様に
OtherModuleView.swiftを参照しているExampleApp.swiftもコンパイルされたが、View1.swift,View2.swiftはコンパイル対象外だった。 - その他に新しいメソッドやプロパティを追加した場合も同様の挙動となる。
まとめ
- 基本は変更したファイルはキャッシュなしでコンパイルされる。
- 検証7 のように新しい定義やメソッド、プロパティを追加すると、それを参照している同一モジュール内のすべてのファイルがコンパイルされる。
- より詳しい話は【iOS】Xcodeのビルドの仕組みを知り、ビルド時間を短縮する方法を探る / #コンパイラの3つのルールが参考になります。
- 意外だったのは
View2.swiftは直接View1.swiftを参照していなくExampleApp.swiftが参照しているだけであってもコンパイル対象となった。- それがコンパイラにとって依存関係があるという判定っぽい。
- おそらくこの仕様が差分ビルドが直感よりも差分じゃなくて重いと感じてしまうのかなと思った。
- ただし、これは別モジュールには影響しないので、そこがマルチモジュール化のメリットになる(別のオーバーヘッドは発生しますが)。
Discussion