Swift & Objective-C

+load vs +initialize:呼び出しタイミングと継承の落とし穴

メソッドスウィズリングのコードは、なぜ決まって+loadの中に入っているのでしょうか?似て見える+initializeに入れてはいけないのでしょうか?

読了 6 分
+load vs +initialize:呼び出しタイミングと継承の落とし穴のカバー画像

メソッドスウィズリングのコードは、なぜ決まって+loadの中に入っているのでしょうか?似て見える+initializeに入れてはいけないのでしょうか?

+load+initializeは、どちらも「クラスの準備が整ったときに一度呼ばれるメソッド」に見えます。しかし、呼び出しタイミングから呼び出し方式、継承ルールまで、すべて異なります。Objective-Cの面接でよく出るテーマでもあり、誤解すると「なぜこのコードが2回実行されるのか」と迷いやすいポイントなので、一度整理しておく価値があります。


早わかり比較表

+load +initialize
呼び出しタイミング クラスがランタイムにロードされたとき(mainより前) クラスが最初のメッセージを受け取る直前(lazy)
呼び出し方式 関数ポインターを直接呼び出す objc_msgSend経由
カテゴリ クラスとカテゴリの実装がそれぞれ呼び出される カテゴリの実装がクラスの実装を上書きする
継承 実装したクラスだけ呼び出される 継承される — サブクラスによって何度も呼ばれることがある
使われなければ? それでも呼ばれる クラスを使わなければ永遠に呼ばれない

表だけでは分かりにくい点を、順に見ていきます。


+load:mainより前に、必ず

+loadは、そのクラス(またはカテゴリ)を含むバイナリがランタイムにロードされた時点、つまりmain関数が実行される前に呼び出されます。アプリでそのクラスを一度も使わなくても呼び出されます。

呼び出し方式が特殊です。objc_msgSendを通さず、関数ポインターで直接呼び出します。そのため、通常のオーバーライドルールは適用されません。

  • サブクラスが+loadを実装していなくても、親の実装が代わりに呼ばれることはありません。
  • クラスの+loadとカテゴリの+loadそれぞれ呼び出されます。上書きではありません。
  • 順序は保証されます。親クラスの+loadが子クラスより先に、クラスの+loadがカテゴリより先に呼ばれます。

この性質はスウィズリングと完全に噛み合います。カテゴリで+loadを実装しても元のクラスの+loadを妨げず、アプリのライフサイクルで最も早い時点に確実に一度実行されるからです。

一方で代償もあります。+loadは**アプリの起動時間にそのまま加算されます。**すべてのクラスの+loadがmainより前に順番に実行されるため、ここで重い処理をすると初期画面の表示が遅れます。Appleが長年「できるだけ+loadを避けるように」と案内している理由です。実際、+loadの中ではselfが属するイメージ以外のクラスがまだロードされていない可能性があり、できることも限られます。

アプリ実行タイムライン上で+loadと+initializeの呼び出しタイミングを示す図
分岐点はmsgSendを通るかどうかだけ

+initialize:最初のメッセージ直前に、遅延実行

+initializeは正反対の戦略です。ランタイムが、クラスが最初のメッセージを受け取る直前に呼び出します。アプリでそのクラスを一度も使わなければ、永遠に呼び出されません。起動時間に負担をかけない、lazyな初期化ポイントです。

ここはobjc_msgSendを経由するため、通常のメソッドと同じように**継承ルールが適用されます。**ここで有名な落とし穴が登場します。

@implementation Animal
+ (void)initialize {
    NSLog(@"initialize: %@", self);
}
@end

@interface Dog : Animal
@end
@implementation Dog
@end

Dogに最初のメッセージを送ると、ログは次のように出力されます。

initialize: Animal
initialize: Dog

Animal+initialize2回実行されます。1回はAnimal自身の分、もう1回は+initializeを実装していないDogが親の実装を継承して実行する分です。そのため、+initializeの慣例的な実装にはクラスチェックが入ります。

+ (void)initialize {
    if (self == [Animal class]) {
        // 本 Animal 来の担当分の初期化だけをここで
    }
}

ちなみに+initializeは、ランタイムがクラス単位でロックを取得して呼び出すため、それ自体がスレッドセーフです。dispatch_onceを重ねて実装する必要はありません。


実務での選択基準

判断基準は簡単です。

  • スウィズリングやクラス登録のように「必ず、できるだけ早く」必要な処理+load(ただし最小限に)
  • そのクラスを使うときだけ必要な準備処理+initialize(selfチェック必須)
  • ほとんどの初期化 → 実はどちらでもなく、dispatch_onceシングルトンやlazyプロパティで十分です

Swiftにはこの悩み自体がありません。Swiftは+loadに相当するものを、そもそも提供していません。mainより前にグローバル実行コードを差し込む公式な経路もありません。その代わり、型プロパティ(static let)が言語レベルでlazyとスレッドセーフを保証し、+initializeの役割を置き換えます。アプリ起動性能の面で、Swiftが構造的に有利な点の一つです。

before mainの+loadとlazyな+initializeを競技場で表現したイラスト
スタート前から走る側と、最初の呼び出しまで眠る側

まとめ

  • +loadmainより前に、必ず、関数ポインターで直接呼び出される — クラスとカテゴリの実装がそれぞれ呼び出されます
  • +initialize最初のメッセージ直前に、lazyに、msgSend経由で呼ばれる — 使われないクラスでは永遠に呼ばれません
  • +initializeは継承によって複数回実行される可能性があるため、if (self == [MyClass class])チェックが慣例です
  • スウィズリングが+loadに置かれる理由:最も早いタイミング + カテゴリに依存しない呼び出し + 確実な1回実行
  • +loadの多用はアプリの起動時間を直接遅らせます — 重い初期化はlazyに先送りするのが正解です

ランタイムシリーズの他の記事(objc_msgSend、メッセージフォワーディング、スウィズリング、KVO)と合わせて読むと、+load+initializeの違いは、結局「msgSendを通るかどうか」という一つの軸で分かれることが分かります。呼び出し経路を理解すれば、ルールを暗記する必要はありません。

あわせて読みたい