Category详解(二)—— 源码层面解析load和initialize方法

959 阅读9分钟

写在前面

+load 和 +initialize 方法大家肯定不陌生,我们的项目中会有很多重写这两个方法的地方,但是你有没有想过他们有什么区别?产生区别的原因是什么?今天我们就从源码的层面来解答一下这些问题。

阅读文章时请注意:留意源码中我增加注释的部分,往往是该函数的重点部分。

问题

针对 load 和 initialize 方法,我们先来思考几个问题:

  1. 分类中有 load 和 initialize 方法吗?
  2. 如果有,那么分类会覆盖原类的方法吗?
  3. 调用顺序是怎样的,子类和父类哪个先?
  4. 调用子类时会不会调用父类的方法?

由于源码较多,将全部源码贴出来不太现实,因此最好的方法是下载runtime源码,跟着文章一起阅读。

+load方法

调用时机

load方法的调用时机是runtime加载类和分类的时候调用。
我们跟上一篇文章一样,定义一个Person类和Person的两个分类

// Person
+ (void)load {
    NSLog(@"person");
}
+ (void)test {
    NSLog(@"person test");
}
 
// Person + Eat
+ (void)load {
    NSLog(@"Person Eat");
}
+(void)test {
    NSLog(@"Person(Eat) test");
}
 
2020-06-01 19:25:49.718843+0800 TestCategory[38073:13350285] person
2020-06-01 19:25:49.719425+0800 TestCategory[38073:13350285] Person Eat
2020-06-01 19:25:49.895610+0800 TestCategory[38073:13350285] Person(Eat) test

可以看到,load方法不论分类还是原类都会被调用,且每个类只会被调用一次,但是test方法却只调用了一次。

test方法只调用一次的原因在上一篇文章中已经解释了,分类的方法会放到list的前面,通过msgSend调用方法时会先查找到分类的方法。
load方法为什么会调用两次呢?我们还是要从runtime源码中寻找答案。

runtime 源码解析

  • 我们同样从 objc-os.mm 中的入口函数 _objc_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)
{
    // 判断是否Categories已经被attach,attach相关详见上期
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }
 
    // 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
    call_load_methods();
}
  • 可以看到调用load方法的函数叫做 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
        // 1. 重复的调用 类 的+load方法直到所有类的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 方法要早调用
  • 调用 +load 方法的地方有两个,一个叫 call_class_loads(),一个叫 call_category_loads(),我们先点进 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;
 
        // typedef void(*load_method_t)(id, SEL);
        // load_method_t是一个指针类型,这里创建一个指针指向该类的load方法的地址
        // 可以看到这里classes实际上是上面的loadable_classes
        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方法的地方,可以看到是通过地址找到相应的函数后调用
        (*load_method)(cls, @selector(load));
    }
     
    // Destroy the detached list.
    if (classes) free(classes);
}

这里就破案了

通过上面的代码可以看出,+load 方法的调用不是通过msgSend,而是直接找到该类 +load 方法的地址,通过地址来调用 +load 方法,因此不会出现只调分类的 +load 方法而不调原类的 +load 方法的情况。

有继承关系时的 +load 调用顺序

实验

我们一共创建4个类,分别是

  1. Person
  2. Person(Eat)
  3. Student
  4. Student(Eat)

经过多次实验得到如下结论:

  1. 原类的 +load 方法总是在分类之前被调用(修改Build Phases → Compile Sources 中的顺序后也是这样)
  2. 父类的 +load 方法总是在子类之前被调用(修改顺序后也是如此)
  3. 分类的父类 +load 方法可能在 分类的子类的 +load方法后才会被调用,和Compile Sources中的顺序有关

为了搞清楚这个结论的原因,我们还是要从runtime的源码入手。

源码

原类

在上面的 call_class_loads 函数中,是从 loadable_classes 中将函数地址一个一个取出来的,因此我们需要搞清 loadable_classes 是怎么来的,以及他的顺序是怎么样的。
由于是从 call_class_loads 函数一步一步往回找,因此这里的调用顺序是倒序的,大家要注意这点。

  • 全局搜索一下关键字 loadable_classes = ,可以发现在 add_class_to_loadable_list(Class cls) 中有出现,我们来看下这个函数的实现:
/***********************************************************************
* 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();
     
    // 如果没有实现 +load 方法,那么不需要添加到list中
    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());
    }
     
    // 可以看到当list容量不够时,扩充的逻辑是 *2 + 16
    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));
    }
     
    // 将class存放到list中,并将method赋值给对应的cls
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}
  • 我们已经知道 add_class_to_loadable_list(cls) 是将该 class 放入list,现在看一下谁调用了 add_class_to_loadable_list,发现在 schedule_class_load(Class cls) 函数中有调用,我们来看一下这个函数的实现

/***********************************************************************
* 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
     
    // #define RW_LOADED             (1<<23)
    // 这里目测是某个标志位,猜测代表已经加入到了list
    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);
}
  • 继续回溯,可以看到是一个叫做 prepare_load_methods(const headerType *mhdr) 调用了该方法。来看一下这个函数的实现
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;
 
    runtimeLock.assertLocked();
 
    // 取到classList
    classref_t const *classlist =
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 执行schedule_class_load,将有 +load 方法的class加入到 loadable_classes 中
        schedule_class_load(remapClass(classlist[i]));
    }
     
    // 分类相关操作,这里暂时不看
    category_t * const *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
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}
  • 再往上回溯,看看谁调用了 prepare_load_methods

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }
 
    // 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
        prepare_load_methods((const headerType *)mh);
    }
 
    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

至此,我们已经把完整的链路打通了,prepare_load_methods 的调用方是 load_images,而 load_images 的调用方是是 objc_init ,我们来理一下方法的调用顺序与作用


那么原类的 +load 方法调用过程的源码我们已经分析完了,接下来让我们来看看分类的 +load 方法的调用过程是怎么样的

分类

还是先看一下 prepare_load_methods(const headerType *mhdr) 函数:


void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;
 
    runtimeLock.assertLocked();
 
    // 原类
    classref_t const *classlist =
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }
 
    // 分类
    // _getObjc2NonlazyCategoryList 函数取出了所有分类,这里没有做任何顺序上的调整,因此是按照编译顺序直接拿到数组
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
 
    // 通过for循环,直接将 categorylist 中的分类加入到loadable_categories中
    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
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
 
        // 添加到 loadable_categories 列表中
        add_category_to_loadable_list(cat);
    }
}

因此可以知道,分类不会受子父类的关系影响,单纯的就是谁先编译谁先调用 +load 方法,我们可以对 Person(Eat) 和 Student(Eat) 调整编译顺序,来验证我们的结论。

如下面两幅图:



+initialize方法

调用时机

先说结论:

  1. +initialize 在方法第一次给类对象发消息的时候会调用。
  2. +initialize 仅会调用一次。
  3. 子类的 +initialilze 方法调用时会先调用父类的 +initialize 方法。
  4. 如果有分类,那么调用顺序和上一章一张,会调用到后编译的分类的 +initialize 方法

用上文中的Person, Person(Eat), Student, Student(Eat) 就可以进行验证,这里就不将验证过程放出来了。
从打印的结果来看是这样的,但是我们需要通过源码进行探究。

源码

  • 我们在 objc-class.mm 这个文件中找到 class_getClassmethod 函数
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
     
    // 可以看到这里是核心
    return class_getInstanceMethod(cls->getMeta(), sel);
}
  • class_getInstanceMethod 才是重点,这个函数传了两个参数,一个是 cls->getMeta(), 一个是 sel,我们点进这个函数看一下
/***********************************************************************
* class_getInstanceMethod.  Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
 
    // This deliberately avoids +initialize because it historically did so.
 
    // This implementation is a bit weird because it's the only place that
    // wants a Method instead of an IMP.
 
    Method meth;
 
    // 此时还没有给这个cls发过消息,因此不会在cache中找到
    meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
    if (meth == (Method)1) {
        // Cache contains forward:: . Stop searching.
        return nil;
    } else if (meth) {
        return meth;
    }
         
    // Search method lists, try method resolver, etc.
    // 这里是核心逻辑,注意这里传递的第四个参数是 LOOKUP_INITIALIZE | LOOKUP_RESOLVER
    lookUpImpOrForward(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
 
    meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
    if (meth == (Method)1) {
        // Cache contains forward:: . Stop searching.
        return nil;
    } else if (meth) {
        return meth;
    }
 
    return _class_getMethod(cls, sel);
}
  • 这里的核心函数是 lookUpImpOrForward(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER),注意最后一个参数,我们点进这个函数中看一下,这里我只放出了关键代码
// 可以看出 behavior & LOOKUP_INITIALIZE 是真, !cls->isInitialized() 也是真,因为没有调用过 initialized 方法
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
     
    // 所以可以走到if判断里,这里是核心逻辑
    cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    // runtimeLock may have been dropped but is now locked again
 
    // 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
}
  • 核心函数是 initializeAndLeaveLocked(cls, inst, runtimeLock),我们一路点进去,最后会进入 initializeAndMaybeRelock,这里只放出核心逻辑
initializeNonMetaClass(nonmeta);
  • 点进 initializeNonMetaClass 函数看一看,这里只放出核心代码
/***********************************************************************
* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
void initializeNonMetaClass(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;
     
    // 这里也是递归调用
    // 这里在找是否有super class,并且super class的initialize方法是否被执行过,如果没执行过,递归调用传递super class
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
     
    // 这里是真正调用initialize方法的地方
    callInitialize(cls);
}
  • 至此,我们终于追踪到了真正调用 +initialize 方法的地方,让我们点进 callInitialize(cls) 函数一探究竟

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
}

终于,真相大白了,我们看到真正调用 +initialize 方法的地方,是通过 objc_msgSend 函数,这也就解释了为什么会调用到分类的 +initialize 方法,详情见上一篇文章。

我们也终于验证了,父类的 +initialize 方法会先调用,且只会被调用到一次。

写在后面

希望大家多多上手尝试,不要觉得看过了就是会了,对于知识需要深层次的挖掘,浅尝辄止是没有用的。

文中如有错误,欢迎指出。