+load vs +initialize(整理筆記)

雷純鋒的技術博客
Objective-C +load vs +initialize

+load+initialize都是用于類的初始化,但是這兩個看是簡單又相似的類方法,在許多方面讓人感到困惑,比如:

  • 子類、父類、分類中相應方法什么時候會被調用
  • 子類中需要顯示的調用父類的實現嗎?
  • 每個方法只調用一次,還是多次?

一. 實例驗證:

舉個?? :

+load方法:

在 main 函數中打印當前 函數名稱:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    NSLog(@"%s",__func__);
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

同時定義Person類和Son類(Son類繼承Person類):

Person類:

#import <Foundation/Foundation.h>

@interface Person : NSObject

@end


#import "Person.h"

@implementation Person
+ (void)load{
    NSLog(@"%s",__func__);
}

+ (void)initialize{
    [super initialize];
    NSLog(@"%s %@",__func__,[self class]);
}

- (instancetype)init{
    if (self = [super init]) {
        NSLog(@"%s",__func__);
    }
      return self;
}

@end

Son類:

#import "Person.h"

@interface Son : Person

@end



#import "Son.h"

@implementation Son
+ (void)load{
    NSLog(@"%s",__func__);
}

+ (void)initialize{
    [super initialize];
    NSLog(@"%s %@",__func__,[self class]);
}

- (instancetype)init{
    if (self = [super init]) {
        NSLog(@"%s",__func__);
    }
    return self;
}
@end

運行輸出:

FJTestProject[29237:1018755] +[Person load]

FJTestProject[29237:1018755] +[Son load]

FJTestProject[29237:1018755] main

從輸出結果可以看出,在沒有對類進行任何操作的情況下,+load方法會被默認執行,并且是在main函數之前執行。

+initialize方法:

同時我們查看下+initialize方法:

#import "Son.h"
#import "Person.h"
#import "ViewController.h"


@interface ViewController ()
@end

@implementation ViewController

#pragma mark --- init method

#pragma mark --- life circle

- (void)viewDidLoad {
    [super viewDidLoad];

    Person *aPerson = [Person new];

    Son  *bSon = [Son new];
}

@end

輸出日志:

FJTestProject[29627:1058200] +[Person load]
FJTestProject[29627:1058200] +[Son load]
FJTestProject[29627:1058200] main
FJTestProject[29627:1058200] +[Person initialize] Person
FJTestProject[29627:1058200] -[Person init]
FJTestProject[29627:1058200] +[Person initialize] Son
FJTestProject[29627:1058200] +[Son initialize] Son
FJTestProject[29627:1058200] -[Person init]
FJTestProject[29627:1058200] -[Son init]

從輸出內容可以看出:

  • +initialize 是通過類似懶加載調用的,如果沒有使用這個類,系統默認不會去掉用這個方法,且默認只加載一次

  • +initialize的調用發生在+init方法之前,創建子類的時候會去調用父類的+ initialize方法。

category 調用順序:

首先為Person類添加類別:

#import "Person+Extention.h"

@implementation Person (Extention)
+ (void)load{
    NSLog(@"%s",__func__);
}

+ (void)initialize{
    [super initialize];
    NSLog(@"%s %@",__func__,[self class]);
}
@end

運行程序,日志如下:

FJTestProject[29751:1066412] +[Person load]
FJTestProject[29751:1066412] +[Son load]
FJTestProject[29751:1066412] +[Person(Extention) load]
FJTestProject[29751:1066412] main
FJTestProject[29751:1066412] +[Person(Extention) initialize] Person
FJTestProject[29751:1066412] -[Person init]
FJTestProject[29751:1066412] +[Person(Extention) initialize] Son
FJTestProject[29751:1066412] +[Son initialize] Son
FJTestProject[29751:1066412] -[Person init]
FJTestProject[29751:1066412] -[Son init]

從日志我們可以看出:
對于+load方法:

  • 會先執行父類中的load方法,再執行子類中的load方法,最后在執行類別的load方法。

對于+ initialize方法:

  • 類別會覆蓋類中的方法,只執行分類的實現。

二. 分析

+ load

  • +load方法是當類或分類被添加到Objective-C runtime時被調用,實現這個方法可以讓我們在類加載的時候執行一些類相關的行為。

  • 子類的+load方法會在它的所有父類的+load方法之后執行,

  • 分類的+load方法會在它的主類的+load方法之后執行。

  • 但是不同類之間的+load方法的調用順序是不確定的。

接著我們打開runtime工程,在objc-runtime-new.mm中我們來看與+load方法相關的關鍵函數。

首先, void prepare_load_methods(header_info *hi)函數:

void prepare_load_methods(header_info *hi)
{
    size_t count, i;

    rwlock_assert_writing(&runtimeLock);

    classref_t *classlist =
    _getObjc2NonlazyClassList(hi, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **categorylist = _getObjc2NonlazyCategoryList(hi, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

這個函數的作用就是提前準備好滿足+load方法調用條件的類和分類,以供接下來調用。其中,在處理類時,調用了同文件中的另一個函數static void schedule_class_load(Class cls)來執行具體的操作。

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED);
}

該函數中的schedule_class_load(cls->superclass);,對入參的父類進行了遞歸調用,以確保父類優先的順序。

void prepare_load_methods(header_info *hi)函數執行完后,當前所有滿足+load方法調用條件的類和分類就被分別存放在全局變量loadable_classesloadable_categories中了。

準備好類和分類后,接下來就是對它們的+load方法進行調用。打開文件objc-loadmethod.m,找到void call_load_methods(void)函數。

void call_load_methods(void)
{
    static BOOL loading = NO;
    BOOL more_categories;

    recursive_mutex_assert_locked(&loadMethodLock);

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

這個函數的作用就是調用上一步準備好的類和分類中的+load方法,并且確保類優先于分類的順序。我們繼續查看在這個函數中調用另外兩個關鍵函數static void call_class_loads(void)static BOOL call_category_loads(void) 。由于這兩個函數的作用大同小異,下面以static void call_class_loads(void)函數為例進行探討。

static void call_class_loads(void)
{
    int i;

    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue;

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }

    // Destroy the detached list.
    if (classes) _free_internal(classes);
 }

這個函數的作用就是真正負責調用類的+load方法,它從全局變量loadable_classes中取出所有可供調用的類,并進行清零操作。

loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
  • loadable_classes指向用于保存類信息的內存首地址
  • loadable_classes_allocated標識已分配的內存空間大小
  • loadable_classes_used則標識已使用的內存空間大小。

然后,循環調用所有類的+load方法。注意,這里是(調用分類的+load方法也是如此)直接使用函數內存地址的方式(*load_method)(cls, SEL_load);+load方法進行調用的,而不是使用發送消息objc_msgSend的方式。

這樣的調用方式就使得+load方法擁有了一個非常有趣的特性,那就是子類、父類和分類中的+load方法的實現是被區別對待的。也就是說如果子類沒有實現+load方法,那么當它被加載時runtime是不會去調用父類的+load方法的。同理,當一個類和它的分類都實現+load方法時,兩個方法都會被調用。因此,我們常常可以利用這個特性做一些"邪惡"的事情比如說方法混淆(Method Swizzling)

+initialize

  • +iniitialize方法是在類或它的子類收到第一條消息之前被調用的,這里所指的消息包括實例方法和類方法的調用。也就是說+initialize方法是以懶加載的方式被調用,如果程序一直沒有給某個類或它的子類發送消息,那么這個類的 +initialize方法是永遠不會被調用的。這樣有利于節省系統資源,避免浪費。

同樣,我們看下runtime的源碼來理解+initialize方法的理解。打開文件objc-runtime-new.mm,找到lookUpImpOrForward函數:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                   bool initialize, bool cache, bool resolver)
{
    ...
        rwlock_unlock_write(&runtimeLock);
    }

    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    // The lock is held to make method-lookup + cache-fill atomic 
    // with respect to method addition. Otherwise, a category could 
    ...
}  

當我們給某個類發送消息時,runtime會調用這個函數在類中查找相應方法的實現或進行消息轉發。從 if (initialize && !cls->isInitialized())判斷我們可以看出,當類沒有初始化時runtime會調用void _class_initialize(Class cls)函數對該類進行初始化。

void _class_initialize(Class cls)
{
    ...
    Class supercls;
    BOOL reallyInitialize = NO;

    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }

    // Try to atomically set CLS_INITIALIZING.
    monitor_enter(&classInitLock);
    if (!cls->isInitialized() && !cls->isInitializing()) {
        cls->setInitializing();
        reallyInitialize = YES;
    }
    monitor_exit(&classInitLock);

    if (reallyInitialize) {
        // We successfully set the CLS_INITIALIZING bit. Initialize the class.

        // Record that we're initializing this class so we can message it.
        _setThisThreadIsInitializingClass(cls);

        // Send the +initialize message.
        // Note that +initialize is sent to the superclass (again) if 
        // this class doesn't implement +initialize. 2157218
        if (PrintInitializing) {
            _objc_inform("INITIALIZE: calling +[%s initialize]",
                          cls->nameForLogging());
        }

        ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);

        if (PrintInitializing) {
            _objc_inform("INITIALIZE: finished +[%s initialize]",
    ...
}
  • 其中,

    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
         _class_initialize(supercls);
    }
    

對入參的父類進行了遞歸調用,以保證父類優先于子類初始化。

  • 另外,最關鍵的是((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);這行代碼暴露了+initialize方法的本質,也就是說runtime使用了發送消息objc_msgSend的方式對+initialize方法進行調用。也就是說+initialize方法的調用與普通方法的調用是一致的,走得都是發送消息的流程。

  • 換言之,如果子類沒有實現+initialize方法,那么繼承自父類的實現會被調用,如果一個分類實現了+initialize方法,那么就會對這個類中的實現造成覆蓋。

因此,如果一個子類沒有實現+initialize方法,那么父類的實現會被執行多次,有時候,這可能不是你想要的;但是如果我們想確保每個類的+initialize方法只執行一次,避免多次執行可能帶來的副作用時,我們可以使用如下代碼:

+ (void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}

三.總結

通過閱讀runtime 的源碼,我們知道了+load+initialize方法實現的細節,明白了它們的調用機制和各自的特點。下面進行各方面對比:

+load VS +initialize

調用時機: 被添加到 runtime 時 VS 收到第一條消息前,可能永遠不調用

調用順序: 父類->子類->分類 VS 父類->子類

調用次數: 1次 VS 多次

是否需要顯式調用父類實現: 否 VS 否

是否沿用父類的實現: 否 VS 是

分類中的實現: 類和分類都執行 VS 覆蓋類中的方法,只執行分類的實現

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容