天天看点

Objective-C的+load方法调用原理分析

Objective-C之Category的底层实现原理

Objective-C的+initialize方法调用原理分析

别人是这么说的

  • 调用时机:

    +load

    方法会在Runtime加载类对象(

    class

    )和分类(

    category

    )的时候调用
  • 调用频率: 每个类对象、分类的

    +load

    方法,在工程的整个生命周期中只调用一次
  • 调用顺序:
    先调用类对象(

    class

    )的

    +load

    方法:
    • 类对象的

      load

      调用顺序是按照 类文件的编译顺序 进行先后调用;
    • 调用子类

      +load

      之前会先调用父类的

      +load

      方法
    再调用分类(

    category

    )的+load方法:按照编译先后顺序调用(先编译的,先被调用)
一、load方法的调用时机和调用频率

+load

方法是在程序一启动运行,加载镜像中的类对象(

class

)和分类(

category

)的时候就会调用,只会调用一次,不论在项目中有没有用到该类对象或者该分类,他们统统都会先被加载进内存,因为类的加载只有一次,所以所有的load方法肯定都会被调用而且只有一次。下面先上一个小demo调试看看:

Objective-C的+load方法调用原理分析
Objective-C的+load方法调用原理分析
Objective-C的+load方法调用原理分析
Objective-C的+load方法调用原理分析
Objective-C的+load方法调用原理分析

上图里面,我创建了一个

person

类,以及它的两个分类–

CLPerson+Test/CLPerson+Test2

,然后给它们都加上两个类方法(

+load/+test

),

main.h

里面先不加任何代码跑跑看。

Objective-C的+load方法调用原理分析

从日志看出,虽然整个工程都没有import过

CLPerson

以及它的两个分类,但是他们的

load

方法还是被调用了,并且都发生在

main

函数开始之前,而且

+test

并没有被调用。所以该现象间接证明了,

load

方法的调用应该和类对象以及分类的加载有关。

main.h

里面调一下

+test

方法

Objective-C的+load方法调用原理分析
Objective-C的+load方法调用原理分析
接下来通过源码分析一下(Runtime源码下载地址)

首先,进入Runtime的初始化文件

objc-os.mm

,找到

_objc_init

函数,该函数可以看作是Runtime的初始化函数。

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

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

忽略一些与本文主题关联不太的函数,直接看最后句

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

其中很明显,

load_images

就是加载镜像/加载模块的意思,应该是与我们话题相关的参数,点进去看看它的实现

/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);

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

           

苹果对该函数官方给出的注释是,处理那些正在进行映射的镜像(images)的+load方法。该方法的实现里面,做了两件事情:

  • prepare_load_methods

    // Discover load methods – 查找并准备load方法,以供后面去调用
  • call_load_methods();

    //Call +load methods – 调用这些load方法

针对上面案例日志中出现的现象,先从结果出发,逆向分析,来看看load方法是如何调用的,进入

call_load_methods();

的实现

/***********************************************************************
* call_load_methods

* Call all pending class and category +load methods.
调用所有的处理中的class和category的+load方法;

* Class +load methods are called superclass-first. 
class的+load方法会被先调用,并且,一个调用一个class的+load方法前,会先对其父类的+load进行调用

* Category +load methods are not called until after the parent class's +load.
category的+load方法的调用,会发生在所有的class的+load方法完成调用之后。
* 
* This method must be RE-ENTRANT, because a +load could trigger 
* more image mapping. In addition, the superclass-first ordering 
* must be preserved in the face of re-entrant calls. Therefore, 
* only the OUTERMOST call of this function will do anything, and 
* that call will handle all loadable classes, even those generated 
* while it was running.
*
* The sequence below preserves +load ordering in the face of 
* image loading during a +load, and make sure that no 
* +load method is forgotten because it was added during 
* a +load call.

* Sequence:调用顺序
* 1. Repeatedly call class +loads until there aren't any more
遍历所有的class对象,调用它们的+load方法,知道所有class中的+load都完成了调用

* 2. Call category +loads ONCE.
调用所有category中的+load方法

* 3. Run more +loads if:
这里我还不太理解,感觉上面都已经把所有的+load调用完了,还不太理解哪里会产生新的+load方法。有待继续补充......
*    (a) there are more classes to load, OR
*    (b) there are some potential category +loads that have 
*        still never been attempted.
* Category +loads are only run once to ensure "parent class first" 
* ordering, even if a category +load triggers a new loadable class 
* and a new loadable category attached to that class. 
*
* Locking: loadMethodLock must be held by the caller 
*   All other locks must not be held.
**********************************************************************/
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;
}
           

很明显,核心逻辑在do-while循环里面,循环中面做了两件事:

  • 首先调用类对象的

    +load

    方法–

    call_class_loads();

    ,直到可加载的类的计数器减到0 –

    loadable_classes_used > 0

  • 然后调用分类的

    +load

    方法--

    call_category_loads();//Call category +loads ONCE

小结A – 程序启动之后,Runtime会在镜像加载阶段,先调用所有类对象的

+load

方法,然后在调用所有分类的

+load

方法,类对象与分类之间参与编译顺序,不会影响上面的结论。例如下图的调试,注意编译顺序
Objective-C的+load方法调用原理分析

这里产生了一个新的疑问:既然是方法调用,为什么

category

+load

方法没有“覆盖”类对象的

+load

方法呢?

有关分类(

category

)中的方法对类对象中的同名方法产生的“覆盖”现象如果还不太清楚,请参考我的Objective-C之Category的底层实现原理一文。

接着上面的源码,继续看看Runtime对于类对象和分类

+load

到底是如何调用的。我们先查看

call_class_loads();

,这是对所有类对象(

class

)的

+load

方法的调用逻辑

/***********************************************************************
* 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;//首先用局部变量loadable_class保存loadable_classes列表
    int used = loadable_classes_used;//在用局部变量used保存loadable_classes_used
    loadable_classes = nil;//将loadable_classes置空
    loadable_classes_allocated = 0;//将loadable_classes_allocated清零
    loadable_classes_used = 0;//将loadable_classes_used清零
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {//遍历classes列表
        Class cls = classes[i].cls;//从列表成员里面获得cls
        load_method_t load_method = (load_method_t)classes[i].method;//从列表成员获取对应cls的+load 的IMP(方法实现)
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);//这里就是对+load方法的调用,注意哦,这是直接的函数调用,不是消息机制那种哦,这里跟类的方法列表什么没关系,直接就是通过+load的IMP进行调用了
    }
    // Destroy the detached list.
    if (classes) free(classes);
}
           

上面实现的主要逻辑发生在for循环里面,该for循环遍历了一个叫

classes

的列表,该列表存储的是一堆

loadable_class

结构体,

loadable_class

的定义如下

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};
           

每一个

struct loadable_class

变量,存储的应该就是

一个类对象

+

一个与该类相关的方法实现

。从

loadable_class

这个命名,说明它内部的信息肯定是表示一个可以被加载的类的相关信息,因此合理推断,它里面的

method

应该就是类的

+load

方法,

cls

就是这个

+load

方法所对应的类对象。这个推断是否正确,我们一会讨论。

我们再看看源码中对于

classes

这个数组进行遍历时到底做了什么。很简单,就是通过函数指针

load_method

loadable_class

中获得

+load

方法的

IMP

作为其参数,然后就直接对其进行调用

(*load_method)(cls, SEL_load);

,所以,类对象的

+load

方法的调用实际上就发生在这里。这里的for循环一旦结束,

classes

所包含的所有类对象的

+load

方法就会被依次调用,这跟一个类是否被在工程项目里被实例化过,是否接受过消息,没有半毛钱关系。

至此,Runtime对于

+load

方法是如何调用的问题我们分析了一半,弄清楚了类对象的

+load

方法的是怎么被一个一个调用的,也就是

static void call_class_loads(void)

这个函数,接下来,还有问题的另一半–

static bool call_category_loads(void)

,也就是关于分类的

+load

方法的调用。进入其中

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

    // Compact detached list (order-preserving)
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[i];
        } else {
            shift++;
        }
    }
    used -= shift;

    // Copy any new +load candidates from the new list to the detached list.
    new_categories_added = (loadable_categories_used > 0);
    for (i = 0; i < loadable_categories_used; i++) {
        if (used == allocated) {
            allocated = allocated*2 + 16;
            cats = (struct loadable_category *)
                realloc(cats, allocated *
                                  sizeof(struct loadable_category));
        }
        cats[used++] = loadable_categories[i];
    }

    // Destroy the new list.
    if (loadable_categories) free(loadable_categories);

    // Reattach the (now augmented) detached list. 
    // But if there's nothing left to load, destroy the list.
    if (used) {
        loadable_categories = cats;
        loadable_categories_used = used;
        loadable_categories_allocated = allocated;
    } else {
        if (cats) free(cats);
        loadable_categories = nil;
        loadable_categories_used = 0;
        loadable_categories_allocated = 0;
    }

    if (PrintLoading) {
        if (loadable_categories_used != 0) {
            _objc_inform("LOAD: %d categories still waiting for +load\n",
                         loadable_categories_used);
        }
    }
    return new_categories_added;
}
           

我们可以看到,这个方法的实现里面,通过系统注释,被划分如下几块:

  • A –

    // Detach current loadable list

    .分离可加载

    category

    列表,也就是把可加载列表的信息保存到本函数的局部变量cats数组上。
  • B –

    // Call all +loads for the detached list

    .消费

    cats

    里面的所有

    +load

    方法(也就是调用它们)
  • C –

    // Compact detached list (order-preserving)

    清理

    cats

    里面已经被消费过的成员,并且更新

    used

    计数值
  • D –

    // Copy any new +load candidates from the new list to the detached list.

    如果又出现了新的可加载的分类,将其相关内容复制到

    cats

    列表上。
  • E –

    // Destroy the new list.

    销毁列表(这里指的是外部的

    loadable_categories

    变量)
  • F –

    // Reattach the (now augmented) detached list. But if there's nothing left to load, destroy the list.

    更新几个记录了category+load信息的几个全局变量。

    相比较于

    call_class_loads

    方法,这里多了步骤C、D、F。关于A、B、E这三个步骤,因为跟

    call_class_loads

    方法里面实现是一样的,不作重复解释。且看看多出来的这几步

    先看C

    Objective-C的+load方法调用原理分析
    对于这个,我画个图演示一下,就明白了
    Objective-C的+load方法调用原理分析
    其实我感觉消费完一轮+load方法之后,cats里面基本上会在这个步骤被清空。

然后我们看看D步骤,如下图

Objective-C的+load方法调用原理分析

其实主要任务就是把新的可以加载的分类(

category

)信息(如果此时发现还有的话)添加到本函数的

cats

数组上。

最后看看F步骤

Objective-C的+load方法调用原理分析
小结B –

Runtime

对于

+load

方法的调用,不是走的我们熟悉的“消息发送”路线,而是直接拿到

+load

方法的

IMP

,直接调用。因此不存在所谓“类的方法被

category

的方法覆盖”的问题,所以除了

结论A

的 类与分类的

+load

方法先后调用顺序外,我们看到类与它的分类的所有的

+load

全部都被调用了,没有被覆盖。

目前,我们确定了类对象的

+load

方法会先于分类的

+load

方法被调用,并且不存在覆盖现象。

  • 那么对于类于类之间

    +load

    调用顺序是怎样的?
  • 同样的疑问对于分类(

    category

    )又是如何呢?

    这两个问题,我们就需要进入

    prepare_load_methods

    方法的实现,看看

    +load

    方法被调用前,Runtime是如何准备它们的。
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);
    }
}
           

上面的实现里,

classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count);

可以看出,利用系统提供的函数

_getObjc2NonlazyClassList

,获得类对象的列表,因为这是系统级别的函数,应该跟编译过程的顺序有关,这里先推测

classlist

中类的顺序与类的编译顺序相同。

接下来,就是遍历

classlist

,对其每个成员通过函数

schedule_class_load()

进行处理

/***********************************************************************
* 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

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

该函数里面就做两件事:

  • 先递归调用自身(

    schedule_class_load()

    ),对当前类(也就是函数传入的参数)的父类进行处理
  • 处理完父类之后,将当前类对象加入到可加载类的相关列表当中

    add_class_to_loadable_list(cls);

经过这样的整理之后,最终整理过的装载类对象相关信息的数组中,父类应该排在子类前面。而不同的类对象之间在数组中的位置,就可以参考它们.m的编译顺序来看了。

每个类对象在被加入数组的时候,会通过

cls->setInfo(RW_LOADED);

设置标签标记一下,这样,如果该类下次被作为父类进行递归调用的时候,就不会重复加入到列表中,保证一个类在数组中只出现一次。

最后再看一下

add_class_to_loadable_list(cls);

里面的逻辑

Objective-C的+load方法调用原理分析

每个步骤的作用请看图中的注释。请注意其中一个细节,第三句代码

method = cls->getLoadMethod();

,进一步查看一下这个

getLoadMethod

Objective-C的+load方法调用原理分析

很明显这个方法就是从一个类对象里面寻找load方法实现,找到的话,就返回load方法的IMP,赋值给method。

Objective-C的+load方法调用原理分析

然后把该类对象

cls

和对应的+load方法IMP

method

赋值给

loadable_classes

列表最后一个成员,该成员我们前面篇章已经说了,该成员就是

loadable_class

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};
           

我们之前推测的说

loadable_class

里面存放的

IMP method;

应该就是

+load

方法的

IMP

,通过上面的分析,证明确实如此。

上面的是针对类对象的

+load

的方法所进行的调用前的整理排布。下面我们看一下分类的

+load

方法是如何处理的。回到

prepare_load_methods

方法,这里我直接贴出相关部分代码截图

Objective-C的+load方法调用原理分析

可以看到,并没像类一样,用一个

schedule

方法进行递归处理,而是直接通过系统函数

_getObjc2NonlazyCategoryList

拿到分类的集合

categorylist

,因为对分类来说,不存在谁是谁的父类,大家都是平级的,而且之前类对象的

+load

方法已经处理过准备好了,所以这里,只需将

categorylist

里面的分类对象一个一个拿出来,通过

add_category_to_loadable_list

方法处理好,一个一个加入到我们后面调用

+load

方法时所用的

loadable_categories

数组里面。

add_category_to_loadable_list(cat)

方法跟上面

add_class_to_loadable_list(cls);

方法里面的逻辑完全一致,不做重复解读。至此,

+load

方法的调用前的前期准备工作,分析完了。

小结C
  • 那么对于类于类之间

    +load

    调用顺序是怎样的?

    调用一个类对象的

    +load

    方法之前,会先调用其父类的

    +load

    方法(如果存在的话),类与类之间,会按照编译的顺序,先后调用其

    +load

    方法。一个类对象的

    +load

    方法不会被重复调用,只可能被调用一次。
  • 同样的疑问对于分类(

    category

    )又是如何呢?

    分类的

    +load

    方法,会按照分类参与编译的顺序,先编译的,先被调用。

我们在通过代码来验证一波。在开篇案例里面,我继续添加几个类和分类,

CLTeacher

CLPerson

子类)、

CLTree

NSObject

子类)、

CLRiver

NSObject

子类)、以及

CLTeacher

的两个分类。

Objective-C的+load方法调用原理分析
  • 首先看出,类对象的

    +load

    方法肯定是先与所有分类的

    +load

    方法被调用的。
  • 分类之间是按照编译的顺序,先后调用

    +load

  • CLTeacher

    CLTree

    CLRiver

    也是按照编译的顺序,先后调用

    +load

    ,由于

    CLPerson

    CLTeacher

    的父类,所以会先用它调用

    +load

    至此,完全和上面的小结C吻合。如果你有兴趣,可以自己尝试一下,变换一下源文件的编译顺序,结果和这里的结论都是一致的。

到这里,关于

+load

方法调用的细节应该就算分析完了~~~

PS:对于苹果的源码,我也在不断的研读和学习中,如果文中有阐述不对的地方,烦请告知指正,于此与各位共勉~~

参考来源

  • Objective-C Class Loading and Initialization
  • 你真的了解load方法么
  • MJ小码哥 OC底层原理班