🛠️
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