Runtime原始碼 +load 和 +initialize

Ly夢k發表於2018-11-01

一、前言

在iOS的開發中,Runtime的方法交換都是寫在+load之中,為什麼不是+initialize中呢?可能不少朋友對此或多或少有一點點的疑問。 我們知道:OC中幾乎所有的類都繼承自NSObject,而+load+initialize用於類的初始化,這兩者的區別和聯絡到底何在呢?接下來我們一起來看看這二者的區別。

二、+load

根據官方文件的描述:

  1. +load是當一個類或者分類被新增到Objective-C執行時呼叫的。
  2. 本類+load的呼叫在其所有的父類+load呼叫之後。
  3. 分類的+load在類的呼叫之後。

也就是說呼叫順序:父類 > 類 > 分類
但是不同類之間的+load方法的呼叫順序是不確定的。

由於+load是在類第一次載入進Runtime執行時才呼叫,由此我們可以知道:

  • 它只會呼叫一次,這也是為什麼麼方法交換寫在+load的原因。
  • 它的呼叫時機在main函式之前。

三、+load的實現

在Runtime原始碼的objc-runtime-new.mmobjc-runtime-old.mm中的load_images方法中都存在這關鍵程式碼:

prepare_load_methods((const headerType *)mh);
複製程式碼

call_load_methods();
複製程式碼

3.1 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);
    }
}
複製程式碼

這裡就是準備好滿足 +load 方法呼叫條件的類和分類,而對classcategory分開做了處理。

  • 在處理class的時候,呼叫了schedule_class_load:
/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed 
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
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); 
}
複製程式碼

從倒數第三句程式碼看出這裡對引數class的父類進行了遞迴呼叫,以此確保父類的優先順序

然後呼叫了add_class_to_loadable_list,把class加到了loadable_classes中:

/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
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++;
}
複製程式碼
  • 對於分類則是呼叫add_category_to_loadable_list把category加入到loadable_categories之中:
/***********************************************************************
* add_category_to_loadable_list
* Category cat's parent class exists and the category has been attached
* to its class. Schedule this category for +load after its parent class
* becomes connected and has its own +load method called.
**********************************************************************/
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}
複製程式碼

3.2 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;
}
複製程式碼

從註釋我們可以明確地看出:

  1. 重複地呼叫class裡面的+load方法
  2. 一次呼叫category裡面的+load方法

還是看一下呼叫具體實現,以call_class_loads為例:

/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
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);
}
複製程式碼

這就是真正負責呼叫類的 +load 方法了。它從上一步獲取到的全域性變數 loadable_classes 中取出所有可供呼叫的類,並進行清零操作。

  • 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

根據官方文件initialize的描述:

  1. Runtime傳送+initialize訊息是在類或者其子類第一次收到訊息時,而且父類會在類之前接收到訊息
  2. +initialize的實現是執行緒安全的,多執行緒下會有執行緒等待
  3. 父類的+initialize可能會被呼叫多次

也就是說 +initialize 方法是以懶載入的方式被呼叫的,如果程式一直沒有給某個類或它的子類傳送訊息,那麼這個類的 +initialize 方法是永遠不會被呼叫的。那這樣設計有什麼好處呢?好處是顯而易見的,那就是節省系統資源,避免浪費。

五、+initialize的實現

在runtime原始碼objc-runtime-new.mm的方法lookUpImpOrForward中有如下程式碼片段:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    ···
    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.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
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) {
        // We successfully set the CLS_INITIALIZING bit. Initialize the class.
        
        // Record that we're initializing this class so we can message it.
        _setThisThreadIsInitializingClass(cls);

        if (MultithreadedForkChild) {
            // LOL JK we don't really call +initialize methods after fork().
            performForkChildInitialize(cls, supercls);
            return;
        }
        
        // 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: thread %p: calling +[%s initialize]",
                         pthread_self(), cls->nameForLogging());
        }

        // Exceptions: A +initialize call that throws an exception 
        // is deemed to be a complete and successful +initialize.
        //
        // Only __OBJC2__ adds these handlers. !__OBJC2__ has a
        // bootstrapping problem of this versus CF's call to
        // objc_exception_set_functions().
        
        // Exceptions: A +initialize call that throws an exception 
        // is deemed to be a complete and successful +initialize.
        //
        // Only __OBJC2__ adds these handlers. !__OBJC2__ has a
        // bootstrapping problem of this versus CF's call to
        // objc_exception_set_functions().
#if __OBJC2__
        @try
#endif
        {
            callInitialize(cls);

            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                             pthread_self(), cls->nameForLogging());
            }
        }
		...
}
複製程式碼

這原始碼我們可以可出結論:

  1. 從前面*的行數知道,_class_initialize方法會對class的父類進行遞迴呼叫,由此可以確保父類優先於子類初始化。
  2. 在截出的程式碼末尾有著如下方法:callInitialize(cls);
void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}
複製程式碼

這裡指明瞭+initialize的呼叫方式是objc_msgSend,它和普通方法一樣是由Runtime通過發訊息的形式,呼叫走的都是傳送訊息的流程。換言之,如果子類沒有實現 +initialize 方法,那麼繼承自父類的實現會被呼叫;如果一個類的分類實現了 +initialize 方法,那麼就會對這個類中的實現造成覆蓋。

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

+ (void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}
複製程式碼

總結

+load +initialize
呼叫時機 載入到runtime時 收到第一條訊息時,可能永遠不呼叫
呼叫方式(本質) 函式呼叫 runtime排程(和普通的方法一樣,通過objc_msgSend)
呼叫順序 父類 > 類 > 分類 父類 > 類
呼叫次數 一次 不定,可能多次可能不呼叫
是否沿用父類的實現
分類的中實現 類和分類都執行 "覆蓋"類中的方法,只執行分類的實現

後記

應該儘可能減少initialize的呼叫,節省資源,擷取官方原文的供大家參考:

Because initialize is called in a blocking manner, it’s important to limit method implementations to the minimum amount of work necessary possible. Specifically, any code that takes locks that might be required by other classes in their initialize methods is liable to lead to deadlocks. Therefore, you should not rely on initialize for complex initialization, and should instead limit it to straightforward, class local initialization.

更多資料:
load
initialize
Objective-C +load vs +initialize

相關文章