NSObject的+load和+initialize詳解

weixin_34116110發表於2017-07-31

文章比較長,第一部分通過runtime的原始碼介紹了load和initialize兩個方法的本質;第二部門通過例項演示了這個兩個方法的呼叫;第三部分就是結論和應用場景

通過runtime原始碼解析load和initialize

+load

6644906-034736b3848e3b2d.png
ADDBC8A1-E2D9-4BCE-8A87-B60FDCC2FB13.png

通過呼叫堆疊,我們可以看出系統首先呼叫的是load_images方法

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();
}
首先是將所有的load搜尋到,放到一個列表中等待呼叫,然後call_load_methods迴圈遍歷呼叫

prepare_load_methods

void prepare_load_methods(const headerType *mhdr)
{
    Module mods;
    unsigned int midx;

    header_info *hi;
    for (hi = FirstHeader; hi; hi = hi->getNext()) {
        if (mhdr == hi->mhdr()) break;
    }
    if (!hi) return;

    if (hi->info()->isReplacement()) {
        // Ignore any classes in this image
        return;
    }

    // Major loop - process all modules in the image
    mods = hi->mod_ptr;
    for (midx = 0; midx < hi->mod_count; midx += 1)
    {
        unsigned int index;

        // Skip module containing no classes
        if (mods[midx].symtab == nil)
            continue;

        // Minor loop - process all the classes in given module
        for (index = 0; index < mods[midx].symtab->cls_def_cnt; index += 1)
        {
            // Locate the class description pointer
            Class cls = (Class)mods[midx].symtab->defs[index];
            if (cls->info & CLS_CONNECTED) {
                schedule_class_load(cls);
            }
        }
    }


    // Major loop - process all modules in the header
    mods = hi->mod_ptr;

    // NOTE: The module and category lists are traversed backwards 
    // to preserve the pre-10.4 processing order. Changing the order 
    // would have a small chance of introducing binary compatibility bugs.
    midx = (unsigned int)hi->mod_count;
    while (midx-- > 0) {
        unsigned int index;
        unsigned int total;
        Symtab symtab = mods[midx].symtab;

        // Nothing to do for a module without a symbol table
        if (mods[midx].symtab == nil)
            continue;
        // Total entries in symbol table (class entries followed
        // by category entries)
        total = mods[midx].symtab->cls_def_cnt +
            mods[midx].symtab->cat_def_cnt;
        
        // Minor loop - register all categories from given module
        index = total;
        while (index-- > mods[midx].symtab->cls_def_cnt) {
            old_category *cat = (old_category *)symtab->defs[index];
            add_category_to_loadable_list((Category)cat);
        }
    }
}

這個方法主要是兩個核心方法schedule_class_load和add_category_to_loadable_list主要乾了兩件事
1、獲取了所有類後,遍歷列表,將其中有+load方法的類加入loadable_class;
2、獲取所有的類別,遍歷列表,將其中有+load方法的類加入loadable_categories.
接下來讓我們看看schedule_class_load

static void schedule_class_load(Class cls)
{
    if (cls->info & CLS_LOADED) return;
    if (cls->superclass) schedule_class_load(cls->superclass);
    add_class_to_loadable_list(cls);
    cls->info |= CLS_LOADED;
}

從以上程式碼可以看出在載入類的load方法的時候,首先是先將父類加入到loadable_class,之後才是子類。所以保證了父類一定是在子類先呼叫

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_load_methods迴圈遍歷,首先是呼叫了class的load方法,然後呼叫了category的方法
接下來讓我們看看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);
}

核心方法是 (*load_method)(cls, SEL_load),typedef void(*load_method_t)(id, SEL);可以看到load_method是一個函式指標,所以是直接呼叫了記憶體地址!

+initialize

6644906-6cb08b0bb9e17df1.png
98779EDD-737F-498D-AD9E-B02508958DDB.png

一樣我們通過呼叫堆疊可以看到系統呼叫的是_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) {
        // 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());
        }

        // Exceptions: A +initialize call that throws an exception 
        // is deemed to be a complete and successful +initialize.
        @try {
            callInitialize(cls);

            if (PrintInitializing) {
                _objc_inform("INITIALIZE: finished +[%s initialize]",
                             cls->nameForLogging());
            }
        }
        @catch (...) {
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: +[%s initialize] threw an exception",
                             cls->nameForLogging());
            }
            @throw;
        }
        @finally {
            // Done initializing. 
            // If the superclass is also done initializing, then update 
            //   the info bits and notify waiting threads.
            // If not, update them later. (This can happen if this +initialize 
            //   was itself triggered from inside a superclass +initialize.)
            monitor_locker_t lock(classInitLock);
            if (!supercls  ||  supercls->isInitialized()) {
                _finishInitializing(cls, supercls);
            } else {
                _finishInitializingAfter(cls, supercls);
            }
        }
        return;
    }
    
    else if (cls->isInitializing()) {
        // We couldn't set INITIALIZING because INITIALIZING was already set.
        // If this thread set it earlier, continue normally.
        // If some other thread set it, block until initialize is done.
        // It's ok if INITIALIZING changes to INITIALIZED while we're here, 
        //   because we safely check for INITIALIZED inside the lock 
        //   before blocking.
        if (_thisThreadIsInitializingClass(cls)) {
            return;
        } else {
            waitForInitializeToComplete(cls);
            return;
        }
    }
    
    else if (cls->isInitialized()) {
        // Set CLS_INITIALIZING failed because someone else already 
        //   initialized the class. Continue normally.
        // NOTE this check must come AFTER the ISINITIALIZING case.
        // Otherwise: Another thread is initializing this class. ISINITIALIZED 
        //   is false. Skip this clause. Then the other thread finishes 
        //   initialization and sets INITIALIZING=no and INITIALIZED=yes. 
        //   Skip the ISINITIALIZING clause. Die horribly.
        return;
    }
    
    else {
        // We shouldn't be here. 
        _objc_fatal("thread-safe class init in objc runtime is buggy!");
    }
}

核心方法就是callInitialize接下來讓我們看看該方法的實現

callInitialize

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

通過該實現我們可以看出其實initialize的本質就是objc_msgSend,所以遵循訊息的轉發機制。


示例演示

Animal 父類

@implementation Animal
+(void)load{
    NSLog(@"Animal load");
}
+(void)initialize{
    NSLog(@"Animal initialize");
}
@end

Dog 子類

+(void)load{
    NSLog(@"Dog load");
}
//分兩種情況,一種註釋掉該程式碼,一種開啟該程式碼
//+(void)initialize{ 
//    NSLog(@"Dog initialize");
//}

**測試程式碼**
````Objective-C
    Animal *animal = [[Animal alloc] init];
    Animal *animal2 = [[Animal alloc] init];
    Animal *animal3 = [[Animal alloc] init];
    Dog *dog = [[Dog alloc] init];
    Dog *dog2 = [[Dog alloc] init];
  • 未註釋程式碼時,列印結果
    2017-07-31 14:24:47.671 test[53134:5737864] Animal load
    2017-07-31 14:24:47.809 test[53134:5737864] Dog load
    2017-07-31 14:24:48.112 test[53134:5737864] Animal initialize
    2017-07-31 14:24:48.112 test[53134:5737864] Dog initialize
  • 註釋掉程式碼時,列印結果
    2017-07-31 14:39:03.916 testaa[53659:5814782] Animal load
    2017-07-31 14:39:03.917 testaa[53659:5814782] Dog load
    2017-07-31 14:39:03.966 testaa[53659:5814782] Animal initialize
    2017-07-31 14:39:03.967 testaa[53659:5814782] Animal initialize

通過兩次列印看到以下現象:

  • 第一次測試load initialize各列印了一次,並且load比initialize提前列印
    原因:+ load是應用一啟動就調動,+ initialize是我們呼叫該類方法的時候才會呼叫,而且這兩個方法理論上只會呼叫一次。
  • 第二次測試Animal的initialize列印了兩次
    原因:initialize遵循的是objc_msgSend訊息的轉發機制,第一次列印是因為我們例項化了Animal並且呼叫了方法;第二次列印是因為Dog子類沒有實現該方法,根據訊息轉發機制的原理,所以會向上查詢父類是否實現了該方法,所以呼叫了父類的initialize的方法了。所以父類的initialize可能會被呼叫多次所以建議以下寫法:
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}

我們可以做個以下嘗試Dog不繼承Animal,然後在Compile Source中將Dog的順序拖到Animal之前。
如下圖:

6644906-88388b9eb906803e.png
D790F51D-98BD-4963-B7A9-D7DEE037FE45.png

可以觀察到Dog的Load在Animal之前列印。所以在沒有繼承關係的時候Load的呼叫順序跟我們的Compile Source的排列順序有關。有繼承關係的,父類一定比子類先呼叫

結論

  • +load方法是在程式一啟動的時候就會呼叫,並且在main函式之前,是根據Xcode中Compile Sources的順序呼叫的。其內部本質是通過函式記憶體地址的方式實現的。所以在有繼承關係的時候子類與父類沒有任何關係,不會相互影響。
  • +initialize方法是我們在第一次使用該類的時候即呼叫某個方法的時候系統開始呼叫 ,是一種懶載入的方式。其內部本質是通過objc_msgSend傳送訊息實現的,因此也擁有objc_msgSend帶來的特性,也就是說子類會繼承父類的方法實現,而分類的實現也會覆蓋元類。

我們常用Method Swizzling建議一定要在Load方法中實現。
stackoverflow使用場景討論

相關文章