NSObject的+load和+initialize详解

文章比较长,第一部分通过runtime的源码介绍了load和initialize两个方法的本质;第二部门通过实例演示了这个两个方法的调用;第三部分就是结论和应用场景

通过runtime源码解析load和initialize

+load

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

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之前。
如下图:

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使用场景讨论

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容