load 與 initialize 方法

+load+initialize 的異同

  1. +load 方法會在 main() 函數之前調用,而 +initialize 是在類第一次使用時才會調用

  2. +load 方法調用優先級:父類>子類>分類,并且不會被覆蓋,均會調用

  3. +initialize 調用優先級:分類>父類,父類>子類,父類的分類重寫了 +initialize方法會覆蓋父類的 +initialize 方法。即:

    • 如果分類和父類均實現了 +initialize,則只有分類的 +initialize 會被調用;
    • 如果父類和子類均實現了 +initialize,第一次引用 子類時,先調用父類的 +initialize,再調用子類的 +initialize
    • 如果父類實現了 +initialize,則第一次引用子類時,會調用兩次父類的 +initialize
  4. +load 方法在 main() 函數之前調用,所有的類文件都會加載,分類也會加載

  5. 均無須顯式調用 super 方法

+load 方法

0x01 load 方法的加載時機

蘋果文檔描述如下

Invoked whenever a class or category is added to the Objective-C runtime.

當 class 或者 category 添加到 runtime 時被喚醒。即 +load 是在這個文件被程序加載時調用,因此 +load 方法是在 main 函數以前調用

0x02 load 方法調用順序
// 定義了如下的類
// 1. TestModel
// 2. TestModel 的子類 SubTestModel
// 3. TestModel Category 類

// 輸出如下
TestModel load
SubTestModel load
TestModel Category load

可以看到系統調用順序是父類->子類->分類

0x03 load 方法的使用場景

由于 +load 方法在 App 啟動加載的時候調用,此時不能保證所有的類被加載完成。

+load 方法是線程安全的,因為內部有鎖,但是也帶了一定的性能開銷。所以一般會在 +load 方法中實現 Method Swizzle

0x04 load 方法實現原理

打斷點查看調用棧

打斷點可以看到 load_images 函數開始加載,在 call_load_methods 調用所有類的 +load 方法。打開 runtime 源碼,這里下載的是 objc4-709,找到 objc-runtime-new.mm 文件,找到 load_images 函數。

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

可以看到調用了 prepare_load_methods 函數,提前準備需要調用的所有 load 函數。

看下 prepare_load_methods 函數內部實現

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertWriting();

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

    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &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);
    }
}

可以看到 prepare_load_methods 內部先獲取 class 的 load 方法,然后才獲取 category 中的 load 方法。因此 load 方法的加載順序為 class->category

繼續看內部調用的 schedule_class_load() 函數

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

這是一個遞歸函數,保證父類先被添加進類表,然后才是類本身。所以 superClass 和 class 的 load 方法加載順序為 superClass->class。

繼續看 add_class_to_loadable_list 函數

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

loadable_classes 是一個全局的結構體, add_class_to_loadable_list 作用是將模塊里所有類的 load 函數存放到loadable_classes 中。load 函數加載完畢,下面看一下 load 函數的調用 call_load_methods() 函數。

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

    loadMethodLock.assertLocked();

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

關鍵函數是 call_class_loads()call_category_loads() 。可以看出,在所有類的 load 方法調用完畢,才會調用 category 的 load 方法

call_class_loads() 的實現如下

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

上面的代碼其實就是從 loadable_classes 中把 load 函數取出來,然后調用。需要注意的是 (*load_method)(cls, SEL_load);,load 方法是直接使用函數指針調用,即走 C 語言函數調用的流程,不是發送消息,并不會走消息轉發流程,也就是說如果一個類實現了 load 函數就會調用,如果沒有實現也不會調用該類的父類 load 函數。

+initialize 方法

0x01 initialize 方法加載時機

蘋果官網描述

Initializes the class before it receives its first message.

這意味著這個函數是懶加載,只有當類接收了第一個消息的時候才會調用 initialize 方法,否則一直不會調用

0x02 initialize 方法的調用順序

父類->子類,分類會覆蓋類,如果子類沒有實現 initialize 方法,父類會調用兩次

  1. 子類實現了 initialize,會先調用父類 initialize,再調用子類 initialize
  2. 子類沒有實現 initialize,父類 initialize 方法會調用兩次
  3. 如果先引用父類的實例對象,再引用子類實例對象,則會在引用父類實例對象時調用父類 initialize 方法;當引用子類實例對象時,由于父類的 initialize 方法已經執行,所以此時只調用子類 initialize 方法
  4. 如果先引用子類的實例對象,再引用父類的實例對象,則會在引用子類的實例對象時,在調用 initialize 方法前,先調用父類 initialize 方法,再調用子類的 initialize 方法;當引用父類實例對象時,由于在引用子類實例對象時已經調用了 initialzie 方法了,此時不再調用 initialize 方法

由于 initialzie 方法可能會被調用多次,所以為保證 initialize 方法只被調用一次,蘋果建議

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

或者使用 dispatch_once

0x03 initialize 方法使用場景

initialize 是線程安全的,有可能阻塞線程,所以 initialize 方法應該限制做一些簡單不復雜的類初始化的前期準備工作

0x04 initialize 方法實現原理

打斷點查看 initialize 方法的調用棧

可以看到在調用 _class_initialize 函數之前,調用了 lookUpImpOrForward 函數,我們先看一下 lookUpImOrForward 函數的實現:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();
    ...
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // 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
    }
    ...    
}

該函數調用了 _class_initialize 函數,看一下 _class_initialize 內部實現:

void _class_initialize(Class cls)
{
    assert(!cls->isMetaClass());

    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_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {
            cls->setInitializing();
            reallyInitialize = YES;
        }
    }   
    if (reallyInitialize) {
            callInitialize(cls);
    }
    ...
}

_class_initialize 函數中調用了 callInitialize 函數,其中的調用順序是從父類開始,到子類的,并且根據 if (supercls && !supercls->isInitialized()) 來看,如果父類已經調用過 initialize 函數,則父類不會再次調用 initialize 函數,對應了前文方法調用順序中的 1、3,if (!cls->isInitialized() && !cls->isInitializing()) 對應前文調用順序中的 4

我們繼續看下 callInitialize 函數做了什么:

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

callInitialize 函數的工作簡單,就是發送消息。這是和 load 函數實現不一樣的地方。load 函數的調用直接是函數指針的調用,而 initialize 函數是消息轉發。所以 class 的子類就算沒有實現 initialize 函數,也會調用父類的 initialize 函數。對應了前文調用順序中的 2

參考

  1. load
  2. initialize
  3. objc-runtime-new
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。