+load vs +initialize(整理筆記)

雷純鋒的技術(shù)博客
Objective-C +load vs +initialize

+load+initialize都是用于類(lèi)的初始化,但是這兩個(gè)看是簡(jiǎn)單又相似的類(lèi)方法讶泰,在許多方面讓人感到困惑咏瑟,比如:

  • 子類(lèi)、父類(lèi)痪署、分類(lèi)中相應(yīng)方法什么時(shí)候會(huì)被調(diào)用
  • 子類(lèi)中需要顯示的調(diào)用父類(lèi)的實(shí)現(xiàn)嗎响蕴?
  • 每個(gè)方法只調(diào)用一次,還是多次惠桃?

一. 實(shí)例驗(yàn)證:

舉個(gè)?? :

+load方法:

在 main 函數(shù)中打印當(dāng)前 函數(shù)名稱(chēng):

#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]));
    }
}

同時(shí)定義Person類(lèi)和Son類(lèi)(Son類(lèi)繼承Person類(lèi)):

Person類(lèi):

#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類(lèi):

#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

運(yùn)行輸出:

FJTestProject[29237:1018755] +[Person load]

FJTestProject[29237:1018755] +[Son load]

FJTestProject[29237:1018755] main

從輸出結(jié)果可以看出,在沒(méi)有對(duì)類(lèi)進(jìn)行任何操作的情況下,+load方法會(huì)被默認(rèn)執(zhí)行辜王,并且是在main函數(shù)之前執(zhí)行劈狐。

+initialize方法:

同時(shí)我們查看下+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]

從輸出內(nèi)容可以看出:

  • +initialize 是通過(guò)類(lèi)似懶加載調(diào)用的,如果沒(méi)有使用這個(gè)類(lèi)呐馆,系統(tǒng)默認(rèn)不會(huì)去掉用這個(gè)方法肥缔,且默認(rèn)只加載一次

  • +initialize的調(diào)用發(fā)生在+init方法之前,創(chuàng)建子類(lèi)的時(shí)候會(huì)去調(diào)用父類(lèi)的+ initialize方法汹来。

category 調(diào)用順序:

首先為Person類(lèi)添加類(lèi)別:

#import "Person+Extention.h"

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

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

運(yùn)行程序续膳,日志如下:

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]

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

  • 會(huì)先執(zhí)行父類(lèi)中的load方法,再執(zhí)行子類(lèi)中的load方法收班,最后在執(zhí)行類(lèi)別的load方法坟岔。

對(duì)于+ initialize方法:

  • 類(lèi)別會(huì)覆蓋類(lèi)中的方法,只執(zhí)行分類(lèi)的實(shí)現(xiàn)摔桦。

二. 分析

+ load

  • +load方法是當(dāng)類(lèi)或分類(lèi)被添加到Objective-C runtime時(shí)被調(diào)用社付,實(shí)現(xiàn)這個(gè)方法可以讓我們?cè)陬?lèi)加載的時(shí)候執(zhí)行一些類(lèi)相關(guān)的行為。

  • 子類(lèi)的+load方法會(huì)在它的所有父類(lèi)的+load方法之后執(zhí)行邻耕,

  • 分類(lèi)的+load方法會(huì)在它的主類(lèi)的+load方法之后執(zhí)行鸥咖。

  • 但是不同類(lèi)之間的+load方法的調(diào)用順序是不確定的。

接著我們打開(kāi)runtime工程兄世,在objc-runtime-new.mm中我們來(lái)看與+load方法相關(guān)的關(guān)鍵函數(shù)啼辣。

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

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);
    }
}

這個(gè)函數(shù)的作用就是提前準(zhǔn)備好滿足+load方法調(diào)用條件的類(lèi)和分類(lèi),以供接下來(lái)調(diào)用御滩。其中鸥拧,在處理類(lèi)時(shí),調(diào)用了同文件中的另一個(gè)函數(shù)static void schedule_class_load(Class cls)來(lái)執(zhí)行具體的操作艾恼。

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);
}

該函數(shù)中的schedule_class_load(cls->superclass);住涉,對(duì)入?yún)⒌母割?lèi)進(jìn)行了遞歸調(diào)用,以確保父類(lèi)優(yōu)先的順序钠绍。

當(dāng)void prepare_load_methods(header_info *hi)函數(shù)執(zhí)行完后舆声,當(dāng)前所有滿足+load方法調(diào)用條件的類(lèi)和分類(lèi)就被分別存放在全局變量loadable_classesloadable_categories中了。

準(zhǔn)備好類(lèi)和分類(lèi)后柳爽,接下來(lái)就是對(duì)它們的+load方法進(jìn)行調(diào)用媳握。打開(kāi)文件objc-loadmethod.m,找到void call_load_methods(void)函數(shù)。

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;
}

這個(gè)函數(shù)的作用就是調(diào)用上一步準(zhǔn)備好的類(lèi)和分類(lèi)中的+load方法磷脯,并且確保類(lèi)優(yōu)先于分類(lèi)的順序蛾找。我們繼續(xù)查看在這個(gè)函數(shù)中調(diào)用另外兩個(gè)關(guān)鍵函數(shù)static void call_class_loads(void)static BOOL call_category_loads(void) 。由于這兩個(gè)函數(shù)的作用大同小異赵誓,下面以static void call_class_loads(void)函數(shù)為例進(jìn)行探討打毛。

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);
 }

這個(gè)函數(shù)的作用就是真正負(fù)責(zé)調(diào)用類(lèi)的+load方法柿赊,它從全局變量loadable_classes中取出所有可供調(diào)用的類(lèi),并進(jìn)行清零操作幻枉。

loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
  • loadable_classes指向用于保存類(lèi)信息的內(nèi)存首地址
  • loadable_classes_allocated標(biāo)識(shí)已分配的內(nèi)存空間大小
  • loadable_classes_used則標(biāo)識(shí)已使用的內(nèi)存空間大小碰声。

然后,循環(huán)調(diào)用所有類(lèi)的+load方法熬甫。注意胰挑,這里是(調(diào)用分類(lèi)的+load方法也是如此)直接使用函數(shù)內(nèi)存地址的方式(*load_method)(cls, SEL_load);對(duì)+load方法進(jìn)行調(diào)用的,而不是使用發(fā)送消息objc_msgSend的方式椿肩。

這樣的調(diào)用方式就使得+load方法擁有了一個(gè)非常有趣的特性瞻颂,那就是子類(lèi)、父類(lèi)和分類(lèi)中的+load方法的實(shí)現(xiàn)是被區(qū)別對(duì)待的郑象。也就是說(shuō)如果子類(lèi)沒(méi)有實(shí)現(xiàn)+load方法贡这,那么當(dāng)它被加載時(shí)runtime是不會(huì)去調(diào)用父類(lèi)的+load方法的。同理扣唱,當(dāng)一個(gè)類(lèi)和它的分類(lèi)都實(shí)現(xiàn)+load方法時(shí)藕坯,兩個(gè)方法都會(huì)被調(diào)用。因此噪沙,我們常沉侗耄可以利用這個(gè)特性做一些"邪惡"的事情比如說(shuō)方法混淆(Method Swizzling)

+initialize

  • +iniitialize方法是在類(lèi)或它的子類(lèi)收到第一條消息之前被調(diào)用的,這里所指的消息包括實(shí)例方法和類(lèi)方法的調(diào)用正歼。也就是說(shuō)+initialize方法是以懶加載的方式被調(diào)用辐马,如果程序一直沒(méi)有給某個(gè)類(lèi)或它的子類(lèi)發(fā)送消息,那么這個(gè)類(lèi)的 +initialize方法是永遠(yuǎn)不會(huì)被調(diào)用的局义。這樣有利于節(jié)省系統(tǒng)資源喜爷,避免浪費(fèi)。

同樣萄唇,我們看下runtime的源碼來(lái)理解+initialize方法的理解檩帐。打開(kāi)文件objc-runtime-new.mm,找到lookUpImpOrForward函數(shù):

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 
    ...
}  

當(dāng)我們給某個(gè)類(lèi)發(fā)送消息時(shí)另萤,runtime會(huì)調(diào)用這個(gè)函數(shù)在類(lèi)中查找相應(yīng)方法的實(shí)現(xiàn)或進(jìn)行消息轉(zhuǎn)發(fā)湃密。從 if (initialize && !cls->isInitialized())判斷我們可以看出,當(dāng)類(lèi)沒(méi)有初始化時(shí)runtime會(huì)調(diào)用void _class_initialize(Class cls)函數(shù)對(duì)該類(lèi)進(jìn)行初始化四敞。

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);
    }
    

對(duì)入?yún)⒌母割?lèi)進(jìn)行了遞歸調(diào)用,以保證父類(lèi)優(yōu)先于子類(lèi)初始化忿危。

  • 另外达箍,最關(guān)鍵的是((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);這行代碼暴露了+initialize方法的本質(zhì),也就是說(shuō)runtime使用了發(fā)送消息objc_msgSend的方式對(duì)+initialize方法進(jìn)行調(diào)用铺厨。也就是說(shuō)+initialize方法的調(diào)用與普通方法的調(diào)用是一致的缎玫,走得都是發(fā)送消息的流程硬纤。

  • 換言之,如果子類(lèi)沒(méi)有實(shí)現(xiàn)+initialize方法碘梢,那么繼承自父類(lèi)的實(shí)現(xiàn)會(huì)被調(diào)用咬摇,如果一個(gè)分類(lèi)實(shí)現(xiàn)了+initialize方法,那么就會(huì)對(duì)這個(gè)類(lèi)中的實(shí)現(xiàn)造成覆蓋煞躬。

因此,如果一個(gè)子類(lèi)沒(méi)有實(shí)現(xiàn)+initialize方法逸邦,那么父類(lèi)的實(shí)現(xiàn)會(huì)被執(zhí)行多次恩沛,有時(shí)候,這可能不是你想要的缕减;但是如果我們想確保每個(gè)類(lèi)的+initialize方法只執(zhí)行一次雷客,避免多次執(zhí)行可能帶來(lái)的副作用時(shí),我們可以使用如下代碼:

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

三.總結(jié)

通過(guò)閱讀runtime 的源碼桥狡,我們知道了+load+initialize方法實(shí)現(xiàn)的細(xì)節(jié)搅裙,明白了它們的調(diào)用機(jī)制和各自的特點(diǎn)。下面進(jìn)行各方面對(duì)比:

+load VS +initialize

調(diào)用時(shí)機(jī): 被添加到 runtime 時(shí) VS 收到第一條消息前裹芝,可能永遠(yuǎn)不調(diào)用

調(diào)用順序: 父類(lèi)->子類(lèi)->分類(lèi) VS 父類(lèi)->子類(lèi)

調(diào)用次數(shù): 1次 VS 多次

是否需要顯式調(diào)用父類(lèi)實(shí)現(xiàn): 否 VS 否

是否沿用父類(lèi)的實(shí)現(xiàn): 否 VS 是

分類(lèi)中的實(shí)現(xiàn): 類(lèi)和分類(lèi)都執(zhí)行 VS 覆蓋類(lèi)中的方法,只執(zhí)行分類(lèi)的實(shí)現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末部逮,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子嫂易,更是在濱河造成了極大的恐慌兄朋,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,080評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怜械,死亡現(xiàn)場(chǎng)離奇詭異颅和,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)缕允,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,422評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)峡扩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人障本,你說(shuō)我怎么就攤上這事教届。” “怎么了彼绷?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,630評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵巍佑,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我寄悯,道長(zhǎng)萤衰,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,554評(píng)論 1 284
  • 正文 為了忘掉前任猜旬,我火速辦了婚禮脆栋,結(jié)果婚禮上倦卖,老公的妹妹穿的比我還像新娘。我一直安慰自己椿争,他們只是感情好怕膛,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,662評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著秦踪,像睡著了一般褐捻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上椅邓,一...
    開(kāi)封第一講書(shū)人閱讀 49,856評(píng)論 1 290
  • 那天柠逞,我揣著相機(jī)與錄音,去河邊找鬼景馁。 笑死板壮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的合住。 我是一名探鬼主播绰精,決...
    沈念sama閱讀 39,014評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼透葛!你這毒婦竟也來(lái)了笨使?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,752評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤获洲,失蹤者是張志新(化名)和其女友劉穎阱表,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體贡珊,經(jīng)...
    沈念sama閱讀 44,212評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡最爬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,541評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了门岔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爱致。...
    茶點(diǎn)故事閱讀 38,687評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖寒随,靈堂內(nèi)的尸體忽然破棺而出糠悯,到底是詐尸還是另有隱情,我是刑警寧澤妻往,帶...
    沈念sama閱讀 34,347評(píng)論 4 331
  • 正文 年R本政府宣布互艾,位于F島的核電站,受9級(jí)特大地震影響讯泣,放射性物質(zhì)發(fā)生泄漏纫普。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,973評(píng)論 3 315
  • 文/蒙蒙 一好渠、第九天 我趴在偏房一處隱蔽的房頂上張望昨稼。 院中可真熱鬧节视,春花似錦、人聲如沸假栓。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,777評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)匾荆。三九已至拌蜘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間牙丽,已是汗流浹背拦坠。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,006評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留剩岳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,406評(píng)論 2 360
  • 正文 我出身青樓入热,卻偏偏與公主長(zhǎng)得像拍棕,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子勺良,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,576評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容