iOS中load和initialize

首發於個人我的博客html

+load方法

建立類和分類

  • 先建立類YZPerson類,而後建立它的兩個分類

YZPerson.m類git

#import "YZPerson.h"

@implementation YZPerson
+(void)run{
    NSLog(@"%s",__func__);
}
+(void)load{
    NSLog(@"%s",__func__);
}
@end
複製代碼

YZPerson+test1.m類github

#import "YZPerson+test1.h"

@implementation YZPerson (test1)

+(void)run{
    NSLog(@"%s",__func__);
}
+(void)load{
    NSLog(@"%s",__func__);
}
@end
複製代碼

YZPerson+test2.m類數組

#import "YZPerson+test2.h"

@implementation YZPerson (test2)

+(void)run{
    NSLog(@"%s",__func__);
}
+(void)load{
    NSLog(@"%s",__func__);
}
@end
複製代碼

建立完以後,這幾個類不主動調用,直接啓動bash

  • 打印結果
CateogryDemo[29670:414343] +[YZPerson load]
CateogryDemo[29670:414343] +[YZPerson(test1) load]
CateogryDemo[29670:414343] +[YZPerson(test2) load]
複製代碼

這說明了。load方法,根本不須要咱們本身調用,編譯完成以後,就會調用。app

疑問

可是有個疑問,由於,原來的類和分類中都寫了load方法,爲啥都調用呢?爲何不是隻調用分類中的呢?函數

查看元類中的方法

利用runtime寫個打印方法

void printMethodNamesOfClass(Class cls)
{
    unsigned int count;
    // 得到方法數組
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存儲方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍歷全部的方法
    for (int i = 0; i < count; i++) {
        // 得到方法
        Method method = methodList[i];
        // 得到方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    // 釋放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}
複製代碼

調用,由於是要查看類方法,因此須要打印元類中的方法

- (void)viewDidLoad {
    [super viewDidLoad];
    printMethodNamesOfClass(object_getClass([YZPerson class]));
}
複製代碼

結果爲

CateogryDemo[30112:420944] +[YZPerson load]
CateogryDemo[30112:420944] +[YZPerson(test1) load]
CateogryDemo[30112:420944] +[YZPerson(test2) load]
CateogryDemo[30112:420944] YZPerson load, run, load, run, load, run,

複製代碼

小結

看得出來,有三個load方法,三個run方法源碼分析

也進一步驗證了,前面查看源碼分析的結論:合併分類的時候,其方法列表等,不會覆蓋掉原來類中的方法,是共存的。性能

源碼分析

同上,先找到初始化方法ui

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

複製代碼

查看 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
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

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

複製代碼

查看加載load的代碼

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 // 1. 調用類的load方法 while (loadable_classes_used > 0) { call_class_loads(); } // 2. Call category +loads ONCE // 2. 調用分類的load方法 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方法,再調用分類的load方法

繼續跟代碼

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

複製代碼

跟着上面註釋中的關鍵代碼,繼續看源碼

typedef void(*load_method_t)(id, SEL);
複製代碼

發現是一個指向函數地址的指針

小結

  • 那麼,答案就很清晰了。+load方法是根據方法地址直接調用,並非通過objc_msgSend函數調用。

有分類和子類的狀況

當有分類,也有子類,而且都有load方法 的狀況下

例如YZStudent是 YZPerson 的子類 YZStudent+test1 是 YZStudent的分類 調用結果爲

CateogryDemo[31904:444099] +[YZPerson load]
CateogryDemo[31904:444099] +[YZStudent load]
CateogryDemo[31904:444099] +[YZStudent(test1) load]
CateogryDemo[31904:444099] +[YZPerson(test1) load]
CateogryDemo[31904:444099] +[YZPerson(test2) load]
複製代碼

源碼分析

接着以前的源碼分析,繼續查看源碼

// 加載鏡像,模塊的方法
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
    {
        mutex_locker_t lock2(runtimeLock);
        // prepare 準備工做
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}
複製代碼
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    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);
    }
}
複製代碼
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);
    // 將cls 添加到 loadable_classes數組最後面
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
複製代碼

能夠看到,load方法,是遞歸調用,傳入父類。依次加入數組中,那麼調用的時候,先調用父類的load,再調用子類的load,並且和編譯順序無關。

總結

+load方法會在runtime加載類、分類時調用

每一個類、分類的+load,在程序運行過程當中只調用一次 load的調用順序

  1. 先調用類的load

    • 先編譯的類,優先調用load
    • 調用子類的load以前,會先調用父類的load
  2. 再調用分類的load

    • 先編譯的分類,優先調用load

+initialize方法

+initialize方法會在類第一次接收到消息時調用

調用順序

先調用父類的+initialize,再調用子類的+initialize (先初始化父類,再初始化子類,每一個類只會初始化1次)

源碼解讀

runtime源碼 objc4源碼解讀過程

>1. objc-msg-arm64.s
- objc_msgSend

>2. objc-runtime-new.mm
- class_getInstanceMethod
- lookUpImpOrNil
- lookUpImpOrForward
- _class_initialize
- callInitialize
- objc_msgSend(cls, SEL_initialize)
複製代碼

具體來看代碼

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
    lookUpImpOrNil(cls, sel, nil, 
                   NO/*initialize*/, NO/*cache*/, YES/*resolver*/);


    return _class_getMethod(cls, sel);
}

複製代碼

繼續查看 lookUpImpOrNil

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}
複製代碼

lookUpImpOrForward 代碼比較長,摘取關鍵代碼以下

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
 if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();

    }
 }
複製代碼

繼續查看關鍵代碼

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

    Class supercls;
    bool reallyInitialize = NO;

    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;
        }
    }
    
    .....
    
    
     callInitialize(cls);
    
 }   
複製代碼

這裏能夠看出,若是一個類,其父類沒有初始化,就遞歸調用該方法進行初始化。最終調用callInitialize進行初始化

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


複製代碼

最終,調用到objc_msgSend 方法,那咱們繼續看objc_msgSend源碼,發現是彙編代碼 截取部分以下:

.data
	.align 3
	.globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
	.fill 16, 8, 0
	.globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
	.fill 256, 8, 0
#endif

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL		

複製代碼

這也說明了,objc_msgSend性能高的緣由,是由於直接操做彙編。

上述流程,用僞代碼表示就是以下,其中 YZStudent 繼承自 YZPerson

if (YZStudent沒有初始化) {
   if(YZPerson沒有初始化){
      objc_msgSend([YZPerson class],@selector(initialize));
    }
    objc_msgSend([YZStudent class],@selector(initialize));
}
複製代碼

注意點

+initialize和+load的很大區別是,+initialize是經過objc_msgSend進行調用的,因此有如下特色

  • 若是子類沒有實現+initialize,會調用父類的+initialize(因此父類的+initialize可能會被調用屢次)
  • 若是分類實現了+initialize,就覆蓋類自己的+initialize調用

對比 +load 和 + initialize 的總結

load、initialize方法的區別什麼?

  1. 調用方式

    • load是根據函數地址直接調用
    • initialize是經過objc_msgSend調用
  2. 調用時刻

    • load是runtime加載類、分類的時候調用(只會調用1次)
    • initialize是類第一次接收到消息的時候調用,每個類只會initialize一次(父類的initialize方法可能會被調用屢次)
  3. load、initialize的調用順序?

    1. load

      1. 先調用類的load

        • 先編譯的類,優先調用load
        • 調用子類的load以前,會先調用父類的load
      2. 再調用分類的load

        • 先編譯的分類,優先調用load
    2. initialize

      1. 先初始化父類
      2. 再初始化子類(可能最終調用的是父類的initialize方法)

本文相關代碼github地址 github

本文參考資料:

runtime源碼

iOS底層原理

更多資料,歡迎關注我的公衆號,不定時分享各類技術文章。

相關文章
相關標籤/搜索