SwiftとObjective-C混合プロジェクトのビルド速度改善方法


概要

古くからあるプロジェクトなど、まだSwift移行が完全にできていない場合など、SwiftとObjective-Cの両方のコードが存在するプロジェクトで有効なビルド速度改善方法を説明します。

Swift-Objective-Cのビルドプロセスに着目した、改善方法の原理も合わせて説明します。

無駄な再ビルドを防止する

まずはSwiftとObjective-Cの混合プロジェクトでビルドが遅くなる原因を説明します。デバッグビルドでは一度ビルドをした後に、再ビルドをする場合、修正箇所の影響範囲に応じて差分ビルドが実行されます。

ビルドを高速化するためには、この差分ビルドの影響範囲を狭め、なるべく多くのファイルを再ビルドさせないことが重要です。また、フルビルドの場合でもコンパイラが考慮すべき依存関係を減らすことで、高速化が見込めます。

ビルドプロセスに着目

では、差分ビルドの影響範囲を狭め、依存関係を減らすためにはどうすべきでしょうか。SwiftとObjective-Cの混合プロジェクトのビルドプロセスに着目してみます。

ビルドプロセス

  1. Objecitve-Cのヘッダファイル(.h)をコンパイル
  2. Bridging Headerをコンパイル(Objecitve-CからSwiftに公開するもの)
  3. Swiftファイルをコンパイル
  4. Generated Headerをコンパイル(SwiftからObjective-Cに公開するもの)
  5. Objecitve-Cの実装ファイル(.m)をコンパイル

上記の通り、Objective-CとSwift間のインタフェースはBridging HeaderとGenerated Headerとなります。そのため、SwiftからはBridging Headerしか見えておらず、Objective-CからはGenerated Headerしか見えていないことになります。

つまり、修正の結果、Bridging HeaderとGenerated Headerに修正が入らなければ、再ビルドの必要はありません。結論としてBridging HeaderとGenerated Headerを必要最低限にし、修正がなるべく入らないようにする工夫が必要です。

Bridging Headerを最小限にする

Bridging Headerでのimportは最低限に

シンプルですが、Bridging HeaderにはSwiftへ公開するヘッダファイル以外は記載しないようにします。過去は使っていたが、もうSwift側で利用しなくなった場合など、Bridging Headerからの消し忘れがないようにします。

プロパティやメソッドの隠蔽

Bridging Headerでimportしているヘッダファイルの中身も最低限にします。
Objective-C側にも公開する必要のないプロパティやメソッドは実装ファイル側に隠蔽します。

プロパティなどはカテゴリにより実装ファイル側に記載できます。

実装ファイル
#import "SomeClass.h"
@interface SomeClass ()
@property (nonatomic, assign) int hoge; // 非公開プロパティ
@end

カテゴリによるヘッダファイル分割

同一クラス内でSwiftに公開するものとそうでないものが混在する場合、ヘッダファイルの分割を検討します。以下のように、カテゴリによりヘッダファイルを分割することができます。

Swift側に公開するヘッダファイル
@interface SomeClass: NSObject
// ...
@end
ObjCのみに公開するヘッダファイル
#import "SomeClass.h"
@interface SomeClass ()
@property (nonatomic, strong) ObjCInternalClass *hoge; // ObjCのみで利用
@end

分割したヘッダファイルがSwift側に公開するもののみBridging Headerに記載します。

Generated Headerを最小限にする

Generated Headerを最小限にするには、Generated Headerを生成するコンパイラディレクティブである@objcをなるべく付与しないようにする、もしくは付与したとしても隠蔽するようにします。

@objc推論をオフに

Swift3まではNSObjectを継承したクラスのプロパティやメソッドなどには自動で@objcが推論により付与されます。一見気づきにくいですが、UIViewControllerを継承したクラスなど、UIKitのクラスはNSObjectを継承していますので、さらにそれらのクラスを継承した場合でも同様です。

Swift4ではビルド設定で@objc推論をオフ(OffまたはDefault)にすることができます。

推論を無効化し、必要最低限の@objcのみ付与します。

privateの利用

@objcを付与する必要があったとしても、Objective-C側のソースやその他のクラスに公開する必要のないプロパティやメソッドはprivateやfileprivateを付与し、外部から隠蔽します。

class Some {
    @objc private var foo: Int
    @objc private func bar() { 
        ///
    }

Swift4環境未満で@objcが自動推論される場合でも同様です。

class Some: UIViewController {
    private var foo: Int
    private func bar() { 
        ///
    }
}

セレクタを利用しない

セレクタを利用する場合、Objective-Cの動的ディスパッチ機能が必要なため、@objcを付与する必要が発生します。NotificationCenterのaddObserverメソッドやTimerのscheduledTimerメソッドなどではコールバック時の処理をセレクタではなくクロージャで実行するようにします。

コールバックにセレクタを利用したAPI
// Bad
func addObserver(_ observer: Any, 
        selector aSelector: Selector, 
            name aName: NSNotification.Name?, 
          object anObject: Any?)
コールバックにクロージャを利用したAPI
// Good
func addObserver(forName name: NSNotification.Name?, 
          object obj: Any?, 
           queue: OperationQueue?, 
           using block: @escaping (Notification) -> Void) -> NSObjectProtocol

参考