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调试看看:
CLPerson
CLPerson+Test
CLPerson_Test2
main.m
上图里面,我创建了一个person
类,以及它的两个分类--CLPerson+Test/CLPerson+Test2
,然后给它们都加上两个类方法(+load/+test
),main.h
里面先不加任何代码跑跑看。
从日志看出,虽然整个工程都没有import过CLPerson
以及它的两个分类,但是他们的load
方法还是被调用了,并且都发生在main
函数开始之前,而且+test
并没有被调用。所以该现象间接证明了,load
方法的调用应该和类对象以及分类的加载有关。
在main.h
里面调一下+test
方法
接下来通过源码分析一下(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
方法,类对象与分类之间参与编译顺序,不会影响上面的结论。例如下图的调试,注意编译顺序
这里产生了一个新的疑问:既然是方法调用,为什么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
对于这个,我画个图演示一下,就明白了
其实我感觉消费完一轮+load方法之后,cats里面基本上会在这个步骤被清空。
然后我们看看D步骤,如下图
其实主要任务就是把新的可以加载的分类(
category
)信息(如果此时发现还有的话)添加到本函数的cats
数组上。
最后看看F步骤
小结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);
里面的逻辑
每个步骤的作用请看图中的注释。请注意其中一个细节,第三句代码
method = cls->getLoadMethod();
,进一步查看一下这个getLoadMethod
拿到类的load方法
很明显这个方法就是从一个类对象里面寻找load方法实现,找到的话,就返回load方法的IMP,赋值给method。
然后把该类对象
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
方法,这里我直接贴出相关部分代码截图
可以看到,并没像类一样,用一个
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
的两个分类。
- 首先看出,类对象的
+load
方法肯定是先与所有分类的+load
方法被调用的。 - 分类之间是按照编译的顺序,先后调用
+load
。 -
CLTeacher
、CLTree
、CLRiver
也是按照编译的顺序,先后调用+load
,由于CLPerson
是CLTeacher
的父类,所以会先用它调用+load
至此,完全和上面的小结C吻合。如果你有兴趣,可以自己尝试一下,变换一下源文件的编译顺序,结果和这里的结论都是一致的。
到这里,关于+load
方法调用的细节应该就算分析完了~~~
PS:对于苹果的源码,我也在不断的研读和学习中,如果文中有阐述不对的地方,烦请告知指正,于此与各位共勉~~