Swift 與 Objective-C

+load 與 +initialize:呼叫時機與繼承陷阱

為什麼方法交換的程式碼總是放在 +load 裡?不能放進看起來很相似的 +initialize 嗎?

閱讀 4 分鐘
+load 與 +initialize:呼叫時機與繼承陷阱 封面圖

為什麼方法交換的程式碼總是放在 +load 裡?不能放進看起來很相似的 +initialize 嗎?

+load+initialize 看起來都像是「類別準備好時呼叫一次的方法」,但從呼叫時機、呼叫方式到繼承規則,全部都不同。這也是 Objective-C 面試常見主題;如果理解錯誤,很容易困惑「為什麼這段程式碼會執行兩次?」值得整理一次。


快速比較表

+load +initialize
呼叫時機 類別在執行階段載入時(main 之前) 類別在收到第一個訊息前(lazy)
呼叫方式 直接呼叫函式指標 經由 objc_msgSend
Category 類別與 category 的實作都會各自呼叫 category 實作會覆寫類別實作
繼承 只會呼叫自行實作的類別 會被繼承 — 可能因為子類別而呼叫多次
如果未使用? 仍然會呼叫 不使用該類別就永遠不會呼叫

接著逐一拆解只看表格不容易掌握的部分。


+load:早於 main,無條件執行

+load會在包含該類別(或 category)的二進位檔由執行階段載入時呼叫,也就是main 函式開始執行之前。即使 App 完全沒有使用該類別,也會被呼叫。

它的呼叫方式很特殊。不經過 objc_msgSend,而是直接透過函式指標呼叫。因此不適用一般的覆寫規則。

  • 子類別沒有實作 +load 時,不會改由父類別實作代為呼叫。
  • 類別的 +load 和 category 的 +load各自呼叫,這不是覆寫。
  • 順序有保證:父類別的 +load 先於子類別,類別的 +load 先於 category。

這些特性正好與方法交換相互配合。即使在 category 中實作 +load,也不會干擾原始類別的 +load,而且能在 App 生命週期最早的時機確實執行一次。

但代價也很明顯。+load會**直接計入 App 啟動時間。**所有類別的+load都會在 main 前依序執行,因此在這裡做繁重工作會延遲第一個畫面。這就是 Apple 多年來建議「盡量避免 +load」的原因。實際上,在+load中,self 所屬映像檔以外的其他類別可能尚未載入,能做的事情也會受到限制。

在 App 執行時間軸上呈現 +load 與 +initialize 呼叫時機的示意圖
分歧點只有一個:是否經過 msgSend

+initialize:第一個訊息前才延遲執行

+initialize採取完全相反的策略。執行階段會在類別收到第一個訊息前呼叫它。如果 App 從未使用該類別,就永遠不會呼叫。這是一個不增加啟動負擔的 lazy 初始化時機。

這裡會經過 objc_msgSend,因此會像一般方法一樣套用**繼承規則。**著名的陷阱正是在這裡出現。

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

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

Dog 傳送第一個訊息後,日誌會如下輸出。

initialize: Animal
initialize: Dog

Animal+initialize執行兩次。一次是 Animal 自己的部分,另一次是未實作+initialize的 Dog 繼承父類別實作後執行。因此,+initialize的慣例實作會加入類別檢查。

+ (void)initialize {
    if (self == [Animal class]) {
        // 只初始化真正 Animal 屬於自己的部分
    }
}

補充一下,+initialize本身具備執行緒安全性,因為執行階段會以類別為單位加鎖後呼叫。不需要再疊加dispatch_once


實務上的選擇標準

判斷標準很簡單。

  • 方法交換、類別註冊等「無條件、越早越好」的工作+load(但要保持最小化)
  • 只有使用該類別時才需要的準備工作+initialize(必須檢查 self)
  • 大多數初始化 → 其實兩者都不是,dispatch_once使用 singleton 或 lazy 屬性就足夠

Swift 根本沒有這個煩惱。Swift 完全不提供相當於+load的機制,也沒有官方管道能在 main 前插入全域執行程式碼。相反地,型別屬性(static let)在語言層級保證 lazy 與執行緒安全,取代+initialize的角色。就 App 啟動效能而言,這是 Swift 在架構上有利的地方之一。

將 main 前的 +load 與 lazy 的 +initialize 表現為賽道的插圖
一方在起跑前就開始奔跑,另一方則睡到第一次呼叫才醒來

總結

  • +load在 main 前無條件地透過函式指標直接呼叫 — 類別與 category 的實作都會各自執行
  • +initialize在第一個訊息前以 lazy 方式經由 msgSend 呼叫 — 未使用的類別永遠不會執行
  • +initialize可能因繼承而執行多次,因此慣例上會進行if (self == [MyClass class])檢查
  • 方法交換放在+load的原因:最早時機 + 不依賴 category 的呼叫 + 確實執行一次
  • 濫用+load會直接拖慢 App 啟動時間 — 繁重初始化應延後到 lazy 執行

搭配執行階段系列的其他文章(objc_msgSend、訊息轉送、方法交換、KVO)一起看,就會發現+load+initialize的差異最終都分歧於同一個軸:是否經過 msgSend。理解呼叫路徑後,就不必死背規則。

延伸閱讀