+load
与 +initialize
的异同
-
+load
方法会在 main() 函数之前调用,而+initialize
是在类第一次使用时才会调用 -
+load
方法调用优先级:父类>子类>分类,并且不会被覆盖,均会调用 -
+initialize
调用优先级:分类>父类,父类>子类,父类的分类重写了+initialize
方法会覆盖父类的+initialize
方法。即:- 如果分类和父类均实现了
+initialize
,则只有分类的+initialize
会被调用; - 如果父类和子类均实现了
+initialize
,第一次引用 子类时,先调用父类的+initialize
,再调用子类的+initialize
; - 如果父类实现了
+initialize
,则第一次引用子类时,会调用两次父类的+initialize
- 如果分类和父类均实现了
-
+load
方法在 main() 函数之前调用,所有的类文件都会加载,分类也会加载 -
均无须显式调用 super 方法
+load
方法
0x01 load 方法的加载时机
苹果文档描述如下
Invoked whenever a class or category is added to the Objective-C runtime.
当 class 或者 category 添加到 runtime 时被唤醒。即 +load
是在这个文件被程序加载时调用,因此 +load
方法是在 main 函数以前调用
0x02 load 方法调用顺序
// 定义了如下的类
// 1. TestModel
// 2. TestModel 的子类 SubTestModel
// 3. TestModel Category 类
// 输出如下
TestModel load
SubTestModel load
TestModel Category load
可以看到系统调用顺序是父类->子类->分类
0x03 load 方法的使用场景
由于 +load
方法在 App 启动加载的时候调用,此时不能保证所有的类被加载完成。
+load
方法是线程安全的,因为内部有锁,但是也带了一定的性能开销。所以一般会在 +load
方法中实现 Method Swizzle
0x04 load 方法实现原理
打断点查看调用栈
打断点可以看到 load_images
函数开始加载,在 call_load_methods
调用所有类的 +load
方法。打开 runtime 源码,这里下载的是 objc4-709,找到 objc-runtime-new.mm 文件,找到 load_images
函数。
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
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
可以看到调用了 prepare_load_methods
函数,提前准备需要调用的所有 load 函数。
看下 prepare_load_methods
函数内部实现
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertWriting();
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);
}
}
可以看到 prepare_load_methods
内部先获取 class 的 load 方法,然后才获取 category 中的 load 方法。因此 load 方法的加载顺序为 class->category
继续看内部调用的 schedule_class_load()
函数
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);
}
这是一个递归函数,保证父类先被添加进类表,然后才是类本身。所以 superClass 和 class 的 load 方法加载顺序为 superClass->class。
继续看 add_class_to_loadable_list
函数
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
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());
}
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));
}
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
loadable_classes
是一个全局的结构体, add_class_to_loadable_list
作用是将模块里所有类的 load 函数存放到loadable_classes
中。load 函数加载完毕,下面看一下 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
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;
}
关键函数是 call_class_loads()
和call_category_loads()
。可以看出,在所有类的 load 方法调用完毕,才会调用 category 的 load 方法
call_class_loads()
的实现如下
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(classes);
}
上面的代码其实就是从 loadable_classes
中把 load 函数取出来,然后调用。需要注意的是 (*load_method)(cls, SEL_load);
,load 方法是直接使用函数指针调用,即走 C 语言函数调用的流程,不是发送消息,并不会走消息转发流程,也就是说如果一个类实现了 load 函数就会调用,如果没有实现也不会调用该类的父类 load 函数。
+initialize
方法
0x01 initialize 方法加载时机
苹果官网描述
Initializes the class before it receives its first message.
这意味着这个函数是懒加载,只有当类接收了第一个消息的时候才会调用 initialize 方法,否则一直不会调用
0x02 initialize 方法的调用顺序
父类->子类,分类会覆盖类,如果子类没有实现 initialize 方法,父类会调用两次
- 子类实现了 initialize,会先调用父类 initialize,再调用子类 initialize
- 子类没有实现 initialize,父类 initialize 方法会调用两次
- 如果先引用父类的实例对象,再引用子类实例对象,则会在引用父类实例对象时调用父类 initialize 方法;当引用子类实例对象时,由于父类的 initialize 方法已经执行,所以此时只调用子类 initialize 方法
- 如果先引用子类的实例对象,再引用父类的实例对象,则会在引用子类的实例对象时,在调用 initialize 方法前,先调用父类 initialize 方法,再调用子类的 initialize 方法;当引用父类实例对象时,由于在引用子类实例对象时已经调用了 initialzie 方法了,此时不再调用 initialize 方法
由于 initialzie 方法可能会被调用多次,所以为保证 initialize 方法只被调用一次,苹果建议
+ (void)initialize {
if (self == [ClassName self]) {
// ... do the initialization ...
}
}
或者使用 dispatch_once
0x03 initialize 方法使用场景
initialize 是线程安全的,有可能阻塞线程,所以 initialize 方法应该限制做一些简单不复杂的类初始化的前期准备工作
0x04 initialize 方法实现原理
打断点查看 initialize 方法的调用栈
可以看到在调用 _class_initialize
函数之前,调用了 lookUpImpOrForward
函数,我们先看一下 lookUpImOrForward
函数的实现:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
...
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// 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
}
...
}
该函数调用了 _class_initialize
函数,看一下 _class_initialize
内部实现:
void _class_initialize(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;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
// Try to atomically set CLS_INITIALIZING.
{
monitor_locker_t lock(classInitLock);
if (!cls->isInitialized() && !cls->isInitializing()) {
cls->setInitializing();
reallyInitialize = YES;
}
}
if (reallyInitialize) {
callInitialize(cls);
}
...
}
_class_initialize
函数中调用了 callInitialize
函数,其中的调用顺序是从父类开始,到子类的,并且根据 if (supercls && !supercls->isInitialized())
来看,如果父类已经调用过 initialize 函数,则父类不会再次调用 initialize 函数,对应了前文方法调用顺序中的 1、3,if (!cls->isInitialized() && !cls->isInitializing())
对应前文调用顺序中的 4
我们继续看下 callInitialize
函数做了什么:
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
callInitialize
函数的工作简单,就是发送消息。这是和 load 函数实现不一样的地方。load 函数的调用直接是函数指针的调用,而 initialize 函数是消息转发。所以 class 的子类就算没有实现 initialize 函数,也会调用父类的 initialize 函数。对应了前文调用顺序中的 2
网友评论