🛠️

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