Objective-C +load vs +initialize

1,768 阅读6分钟

之前有了解过+load 和 +initialize两个方法,但是一直以来没有总结,本篇文章将从以下几个方面着手:

  • 方法调用时机

  • 父类,子类,分类中的调用顺序

    load方法

    load方法在类被加入到objective-C runtime 中时调用,它在main方法调用之前,而且只会被调用一次。子类的load方法会在父类之后调用,category里的load方法会在本类之后调用。

    在runtime的源代码中找到文件objc-runtime-new.mm,然后找到 void prepare_load_methods(header_info *hi) 函数

    void prepare_load_methods(header_info *hi)
    {
    size_t count, i;
    rwlock_assert_writing(&runtimeLock);
    classref_t *classlist =
        _getObjc2NonlazyClassList(hi, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
       }
    
    category_t **categorylist = _getObjc2NonlazyCategoryList(hi, &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方法调用条件的类和分类,以供接下来调用,其中在处理类时,调用了另外一个函数 static void schedule_class_load(Class cls) 它的实现时这样的

    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);
    }
    
      schedule_class_load(cls->superclass);
    

这个函数对父类进行递归操作,以确保父类优先执行。 准备好类和分类后,接下来就是对他们的+load方法进行调用了。打开objc-loadmethod.m 找到其中的void call_load_methods(void) 函数

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

    recursive_mutex_assert_locked(&loadMethodLock);

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

很明显,大概的逻辑就是,调用上一步准备好的类和分类中的+load方法,并且先调用类后调用分类。我们查看这个函数中调用的一个关键函数 static void call_class_loads(void)

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

其中 (*load_method)(cls, SEL_load),直接使用函数内存地址的方式,而不是使用消息发送的objc_msgsend的方式。这样的调用方式就使得 +load 方法拥有了一个非常有趣的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。(这句有误:子类没实现,父类也会调用)也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。因此,我们常常可以利用这个特性做一些“邪恶”的事情,比如说方法混淆(Method Swizzling)。

+ initialize

+initialize 方法在类和父类收到第一条消息前被调用,也就是以懒加载的方式被调用,这样的好处是节约系统资源,避免浪费。结合runtime的源码加深对+initialize的调用

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                       bool initialize, bool cache, bool resolver)
{
    ...
        rwlock_unlock_write(&runtimeLock);
    }

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

    // The lock is held to make method-lookup + cache-fill atomic 
    // with respect to method addition. Otherwise, a category could 
    ...
}

以上是消息转发流程,从代码中可以看到,当类没有初始化时,runtime会调用void _class_initialize(Class cls) 对该类进行初始化。

void _class_initialize(Class cls)
{
    ...
    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_enter(&classInitLock);
    if (!cls->isInitialized() && !cls->isInitializing()) {
        cls->setInitializing();
        reallyInitialize = YES;
    }
    monitor_exit(&classInitLock);

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

        ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);

        if (PrintInitializing) {
            _objc_inform("INITIALIZE: finished +[%s initialize]",
    ...
}

虽然不能完全理解,但是我们可以看到代码中对父类进行了递归操作,以确保父类优先于子类初始化。另外最关键对是,runtime使用了发送消息objc_msgsend的方法对+initialize方法进行调用,也就是说它走的是发送消息对流程。如果子类没有实现这个方法,父类对实现就会被调用,如果分类实现了该方法,那么就会对这个类对实现进行覆盖。这个和+load方法是不同的。 因此,如果一个子类没有实现+initialize方法,那么父类的实现就会被多次执行。我们可以用下面的代码实现。

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

结合demo进行验证 定义父类Person,子类Man,Woman,Man的分类Man+nice并且加入如下打印;

@implementation Person
 
+(void)load{
    NSLog(@"%s",__func__);
}

@end
@implementation Man
 
+(void)load{
    NSLog(@"%s",__func__);
}

@end
@implementation Man (nice)
 
+(void)load{
    NSLog(@"%s",__func__);
}

@end

打印结果如下

从结果可以看出,父类,子类和分类中的load方法互不影响,分类和分类也互不影响,都会执行。

1.子类没有实现,会不会调用父类的initlize方法

@implementation Person
 
+ (void)initialize{
    NSLog(@"%s",__func__);
}
@end
@implementation Man
@end
@implementation Woman
@end
@implementation AViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
    Man* m = [Man new];
    Woman* w = [Woman new];
}

打印结果

不难看出,如果子类没有实现,会进行消息转发,调用父类的,可是为什么是三次,不是两次呢,因为父类本身也要初始化一次,后两次才是子类消息转发到父类的。

2.分类实现,会覆盖本类吗?

@implementation Man (nice)

+ (void)initialize{
 NSLog(@"%s",__func__);
}
@end
@implementation Man

+ (void)initialize{
    NSLog(@"%s",__func__);
}
@end

可以看出分类会覆盖子类。 3.多个分类呢

@implementation Man (nice)

+ (void)initialize{
 NSLog(@"%s",__func__);
}
@end
@implementation Man

+ (void)initialize{
    NSLog(@"%s",__func__);
}
@end
@implementation Man (Bad)

+ (void)initialize{
    NSLog(@"%s",__func__);
}
 
@end

多个分类会互相覆盖,顺序是随机的。

总结

通过查阅runtim的源码,我们知道了+load和+initialize方法的实现细节。明白了他们的调用机制和各自特点。总结如下

项目 +load +initialize
调用时机 被添加到runtime时main函数前 懒加载调用类的某个方法时
调用次数 1 多次
调用顺序 父类->子类->分类 父类->子类
调用细节 通过函数内存地址调用 objc_msgSend
分类中的实现 类和分类都会执行 覆盖类中的方法

load方法 调用顺序 | 父类->子类->分类 通过函数内存地址调用 所以父类,子类,不会覆盖 initlize 调用顺序:父类子类 走的是消息转发机制,所以如果子类没实现,父类可能会多次调用。 分类会覆盖类中对方法。 当方法替换时多用load方法,也可以用在initlize中,但是要注意会分类实现了,本类就不再调用,而且只有一个分类可以调用。

参考链接: blog.leichunfeng.com/blog/2015/0…