Objective-C 中绝大部分的类都继承自 NSObject 类。而在 NSObject 类中有两个特殊的类方法 + load 和 + initialize,用于类的初始化。
load 和 initialize 的共同特点
load 和 initialize 有很多共同点,比如:
1. 在不考虑开发者主动使用的情况下,系统最多调用一次
2. 如果父类和子类都被调用,父类的调用一定在子类之前
3. 都是为了应用运行时提前创建合适的运行环境
load 方法
+load 方法是当类或分类被添加到 OC runtime 时被调用,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。子类的 +load 方法会在它的所有父类的 +load 方法之后执行,而分类的 +load 是在它的主类的 +load 方法之后执行。但是不同类之间的 +load 方法的调用顺序是不确定的。
总结为以下几点:
- 调用时机比较早,运行环境有不确定因素。具体说来,在 iOS 上通常就是 APP 启动时进行加载,但当 load 调用的时候,并不能保证所有类都加载完成且可用,必要时还要自己负责做 auto release 处理。
- 对于有依赖关系的两个库中,被依赖的类的load 会优先调用。但是在一个库之内,调用顺序是不确定的。
- 对于一个类,没有 load 方法实现就不会调用,不会考虑对 NSObject 的继承。
- 一个类的load 方法不用写明[super load], 父类就会收到调用,并且在子类之前。
- Catagory 的 load 也会收到调用,但是顺序在主类的 load 调用之后。
- 不会直接触发 initialize 的调用
- 调用类的 load 方法,它是直接使用函数内存地址的方式 (*load_method)(cls, SEL_load);
对 +load 方法进行调用的,而不是使用消息 objc_msgSend 的方式。这种调用方式也就使得子类、分类以及父类的 +load 方法的实现被区别对待了
initialize 方法
+initialize 方法是在类或者它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 + initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或者它的子类发送消息,那么这个类的 + initialize 方法永远不会被调用。这样,可以节省系统资源,避免浪费。
总结为以下几点:
- initialize 的自然调用是在第一次主动使用当前类的时候(以懒加载的方式)
- 在 initialize 方法收到调用时,运行环境基本健全
- initialize 的运行过程中,是保证线程安全的。
- 和 load 不同,即使子类不实现 initialize 方法,会把父类的实现继承过来调用一遍。
- runtime 使用了发送消息 objc_msgSend
的方式对 +initialize
方法进行调用
由于 initialize 的这些特点,使得其应用要比 load 要稍微广泛一些,可用作一些初始化工作,或者单例模式的一种实现方案。
从源码学习 load 和 initialize
+load 方法
打开 runtime 工程,我们接下来看看与 +load 方法相关的几个关键函数。首先是文件 objc-runtime-new.mm
中的 void prepare_load_methods(header_info *hi)
函数:
// 该函数提前准备好满足 load 方法调用条件的类和分类,以供接下来的调用。
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);
}
}
其中,在处理类的时候,调用了同文件中的另一个函数 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;
// 确保父类优先
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
在这个函数中,我们发现对于 弗雷采用递归调用,以确保父类优先执行。
当 void prepare_load_methods(header_info *hi)
函数执行完后,当前所有满足 +load
方法调用条件的类和分类就被分别存放在全局变量 loadable_classes
和 loadable_categories
中。
准备好类和分类后,接下来就是对它们的 +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 BOOL call_category_loads(void)
。由于这两个函数的作用大同小异,下面就以篇幅较小的 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 中取出所有可供调用的类,并进行清零操作。
loadable_classes = nil; // 指向用于保存类信息的内存的首地址
loadable_classes_allocated = 0; // 标识已分配的内存空间大小
loadable_classes_used = 0; // 标识已使用的内存空间大小
// 循环调用所有类的 +load 方法
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method; //直接使用函数内存地址的方式 (*load_method)(cls, SEL_load); 对 +load 方法进行调用的
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 方法。
+initialize 方法
打开文件 objc-runtime-new.mm ,找到以下函数:
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 会调用这个函数在类中查找相应方法的实现或进行消息转发。当类没有初始化时 runtime 会调用 void _class_initialize(Class cls)
函数对该类进行初始化,函数如下:
void _class_initialize(Class cls)
{
...
Class supercls;
BOOL reallyInitialize = NO;
// 对入参的父类进行递归调用,以确保父类优先于子类初始化
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());
}
// runtime 使用了发送消息 objc_msgSend 的方式对 +initialize 方法进行调用
// 如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;
// 如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
if (PrintInitializing) {
_objc_inform("INITIALIZE: finished +[%s initialize]",
...
}
总结
建议大家从 runtime 源码 出发,去具体了解一下 +load 和 +initialize 方法实现的细节,明白它们的调用机制。通过一张表,我们来总结一下这篇文章。
方法 | +load | +initialize |
---|---|---|
调用时机 | 被添加到 runtime 时 | 收到第一条消息前,可能永远不调用 |
调用顺序 | 父类->子类->分类 | 父类->子类 |
调用次数 | 1次 | 多次 |
是否需要显式调用父类实现 | 否 | 否 |
是否沿用父类的实现 | 否 | 是 |
分类中的实现 | 类和方法都执行 | 覆盖类中的方法,只执行分类的实现 |