APP 启动到执行 main 函数之前,程序就执行了很多代码。

执行顺序:

  1. 将程序依赖的动态链接库加载到内存
  2. 加载可执行文件中的所有符号,代码 runtime 解析被编译的符号代码
  3. 遍历所有的 class
  4. 按继承层级一次调用 Class 的 load 和 category 的 load 方法。

一、+load

+load 方法是当类或分类被添加到 Objective-C runtime 时被调用的,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。子类的 +load 方法会在它的所有父类的 +load 方法之后执行,而分类的 +load 方法会在它的主类的 +load 方法之后执行。但是不同的类之间的 +load 方法的调用顺序是不确定的。

打开 runtime 工程,看看与 +load 方法相关的几个关键函数。首先是文件 objc-runtime-new.mm 中的 void prepare_load_methods(header_info *hi) 函数:

/**
* @brief 执行类的 +load 方法
*/
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++) {
// 调用类的 +load
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());
// 添加到可执行 +load 的分类列表中
add_category_to_loadable_list(cat);
}
}

这个函数的作用就是提前准备好满足 +load 方法调用条件的类和分类,以供接下来的调用。其中,在处理类时,调用了同文件中的另外一个函数 static void schedule_class_load(Class cls) 来执行具体的操作。

// 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
// 父类调用 +load
schedule_class_load(cls->superclass);
// 添加到可执行 +load 的类列表中
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}

其中,schedule_class_load(cls->superclass); 确保父类优先的顺序。void prepare_load_methods(header_info *hi) 函数执行完后,当前所有满足 +load 方法调用条件的类和分类就被分别存放在全局变量 loadable_classesloadable_categories 中了。

准备好类和分类后,接下来就是对它们的 +load 方法进行调用了。打开文件 objc-loadmethod.m,找到其中的 void call_load_methods(void) 函数。

/**
* @brief 调用 load 方法
*/
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) {
// 先调用类的 load
call_class_loads();
} // 2. Call category +loads ONCE
// 再调用分类的 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 方法,并且确保类优先于分类的顺序。继续查看在这个函数中调用的另外两个关键函数 static void call_class_loads(void)static BOOL call_category_loads(void)

/**
* @brief 调用类的 load 方法
*/
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 方法实现
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, SEL_load);
} // Destroy the detached list.
if (classes)
free(classes);
}

这个函数的作用就是真正负责调用类的 +load 方法了。它从全局变量 loadable_classes 中取出所有可供调用的类,并进行清零操作。

loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;

其中 loadable_classes 指向用于保存类信息的内存的首地址,loadable_classes_allocated 标识已分配的内存空间大小,loadable_classes_used 则标识已使用的内存空间大小。

然后,循环调用所有类的 +load 方法。注意,这里是(调用分类的 +load 方法也是如此)直接使用函数内存地址的方式 (*load_method)(cls, SEL_load); 对 +load 方法进行调用的,而不是使用发送消息 objc_msgSend 的方式。

这样的调用方式就使得 +load 方法拥有了一个非常有趣的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。因此,我们常常可以利用这个特性做一些“邪恶”的事情,比如说方法混淆(Method Swizzling)。

二、+initialize

+initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 +initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize 方法是永远不会被调用的。这样设计节省系统资源,避免浪费。

同样的,我们还是结合 runtime 的源码来加深对 +initialize 方法的理解。打开文件 objc-runtime-new.mm,找到以下函数:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO; runtimeLock.assertUnlocked(); // Optimistic cache lookup 缓存查找
if (cache) {
// 先去当前缓存查找,方法缓存存在什么地方?
// 缓存列表 cache_t,在 class 结构体中
imp = cache_getImp(cls, sel);
// 缓存中找到直接返回
if (imp)
return imp;
} // runtimeLock is held during isRealized and isInitialized checking to prevent races against concurrent realization.
// runtimeLock 在 isRealized 和 isInitialized 检查过程中被持有,以防止对并发实现的竞争。 // runtimeLock is held during method search to make method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because the cache was re-filled with the old value after the cache flush on behalf of the category.
// runtimeLock 在方法搜索期间被保持,以使方法查找+关于缓存填充的方法添加原子化。否则,可以添加类别,但会无限期忽略该类别,因为在代表该类别刷新缓存后,缓存将重新填充旧值。 runtimeLock.lock();
// 查找是否是已知的类
checkIsKnownClass(cls); if (!cls->isRealized()) {
realizeClass(cls);
} if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
// 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
} retry:
runtimeLock.assertLocked(); // Try this class's cache. imp = cache_getImp(cls, sel);
if (imp) goto done; // Try this class's method lists.
{
// 查找 Method
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 添加到缓存当中
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
} // Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
// 移动 class 指针
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
} // Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
} // Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
} // No implementation found. Try method resolver once.
// 没有找到方法实现,尝试动态解析
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
} // No implementation found, and method resolver didn't help.
// Use forwarding.
// _objc_msgForward 当前触发消息转发
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst); done:
runtimeLock.unlock(); return imp;
}

当给某个类发送消息时,runtime 会调用这个函数在类中查找相应方法的实现或进行消息转发。从上可以看出,当类没有初始化时 runtime 会调用 void _class_initialize(Class cls) 函数对该类进行初始化。

/**
* @brief 向任意未初始化的类发送 +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;
// 父类存在 && 父类还没有执行 initialized 方法
// 有时候父类的 initialize 方法会被调用多次,这是由于当子类没有实现 initialize 方法时,会先调用父类的 initialize 方法(第一次),然后再调用自己的 initialize 方法,由于是通过 obj_msgSend 消息机制调用,通过 isa 找到类对象,如果没有则去父类中查找,找到再调用(第二次)
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) {
// We successfully set the CLS_INITIALIZING bit. Initialize the class. // Record that we're initializing this class so we can message it.
_setThisThreadIsInitializingClass(cls); if (MultithreadedForkChild) {
// LOL JK we don't really call +initialize methods after fork().
performForkChildInitialize(cls, supercls);
return;
} // 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: thread %p: calling +[%s initialize]",
pthread_self(), cls->nameForLogging());
} // Exceptions: A +initialize call that throws an exception
// is deemed to be a complete and successful +initialize.
//
// Only __OBJC2__ adds these handlers. !__OBJC2__ has a
// bootstrapping problem of this versus CF's call to
// objc_exception_set_functions().
#if __OBJC2__
@try
#endif
{
// 调用 +initialize 消息
callInitialize(cls); if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
pthread_self(), cls->nameForLogging());
}
}
#if __OBJC2__
@catch (...) {
if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: +[%s initialize] "
"threw an exception",
pthread_self(), cls->nameForLogging());
}
@throw;
}
@finally
#endif
{
// Done initializing.
lockAndFinishInitializing(cls, supercls);
}
return;
}
...
}

其中,代码对入参的父类进行了递归调用,以确保父类优先于子类初始化。另外,最关键的是 callInitialize

/**
* @brief initialize 是通过消息发送机制调用,消息发送机制通过 isa 指针找到对应的方法与实现,因此先找到分类方法中的实现,会优先调用分类方法中的实现。
*/
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}

runtime 使用了发送消息 objc_msgSend 的方式对 +initialize 方法进行调用。也就是说 +initialize 方法的调用与普通方法的调用是一样的,走的都是发送消息的流程。换言之,如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖。

因此,如果一个子类没有实现 +initialize 方法,那么父类的实现是会被执行多次的。有时候,这可能是你想要的;但如果我们想确保自己的 +initialize 方法只执行一次,避免多次执行可能带来的副作用时,我们可以使用下面的代码来实现:

+ (void)initialize {
if (self == [ClassName self]) {
// ... do the initialization ...
}
}

三、总结

+(void)initialize 与 +(void)load 两个方法的比较

+load +initialize
调用时机 被添加 runtime 时 收到第一条消息时,也可能永远不调用
调用顺序 父类 -> 子类 -> 分类 父类 -> 子类
调用次数 1次 系统执行 1 次,手动可以调用多次
是否需要显式调用父类实现
是否沿用父类的实现
分类中的实现 类和分类都执行 分类覆盖类中的实现

initialize 方法的调用是线程安全的。

+load的执行顺序:

  1. 对于有依赖关系的两个库中,被依赖的类的 +load 会优先调用。但在一个库之内,调用顺序是不确定的。
  2. 一个类的 +load 方法不用写明 [super load],父类就会收到调用,并且在子类之前。也就是执行子类的load方法之前,当父类未加载时会先执行父类的 Load 方法。
  3. 分类 category 的方法在最后执行
  4. 执行完上面的才按 compile sources 的顺序执行 load。
  5. 对于一个类而言,没有 load 方法实现就不会调用,不会考虑对 NSObject 的继承。

+initialize 的执行顺序:

  1. +initialize 的自然调用是在第一次主动使用当前类的时候。
  2. 在 +initialize 方法收到调用时,运行环境基本健全。
  3. initialize 的运行过程中是能保证线程安全的。
  4. 和 load 不同,即使子类不实现 initialize 方法,会把父类的实现继承过来调用一遍。注意的是在此之前,父类的方法已经被执行过一次了,同样不需要 super 调用。

相同点:

  1. 在不考虑开发者主动使用的情况下,系统最多会调用一次
  2. 父类在子类之前被调用。
  3. 都是为了应用运行提前创建合适的运行环境

不同点:

  1. load 方法会在加载类的时候就被调用,也就是 ios 应用启动时就会加载所有的类,就会调用每个类的 +load 方法;initialize 方法会在第一次初始化这个类之 前被调用,我们用它来初始化静态变量。
  2. load会在 main() 函数之前调用,+initialize 则在类实例化或调用类方法时调用。load 顺序在 initialize 之前。
  3. 如果子类中没有 +initialize 方法,则会再次调用父类的 +initialize 方法。
  4. 类别会覆盖主类的 +initialize 方法,+load 方法则不会被覆盖。
  5. +initialize 方法的调用看起来会更合理,通常在它里面写代码比在 + load 里写更好,因为它是懒调用的,是有可能完全不被调用的。
  6. 类接收消息时,运行时会先检查 + initialize 有没有被调用过。如果没有,则会在消息被处理前调用。
  7. initialize 最终是通过 objc_msgSend 来执行的,objc_msgSend 会执行一系列方法查找,并且 Category 的方法会覆盖类中的方法;load 是在被添加到 runtime 时开始执行,父类最先执行,然后是子类,最后是 Category。又因为是直接获取函数指针来执行,不会像 objc_msgSend 一样会有方法查找的过程。

四、内容来源

雷纯锋的技术博客 - Objective-C +load vs +initialize

+load 和 +initialize的更多相关文章

  1. load和initialize方法

      一.load 方法什么时候调用: 在main方法还没执行的时候 就会 加载所有类,调用所有类的load方法. load方法是线程安全的,它使用了锁,我们应该避免线程阻塞在load方法. 在项目中使 ...

  2. OC load与initialize

    OC load与initialize load 当类被引用进程序的时候会执行这个函数 一个类的load方法不用写明[super load],父类就会收到调用,并且在子类之前. Category的loa ...

  3. load与initialize

    NSObject类有两种初始化方式load和initialize load + (void)load; 对于加入运行期系统的类及分类,必定会调用此方法,且仅调用一次. iOS会在应用程序启动的时候调用 ...

  4. Objective C类方法load和initialize的区别

    Objective C类方法load和initialize的区别   过去两个星期里,为了完成一个工作,接触到了NSObject中非常特别的两个类方法(Class Method).它们的特别之处,在于 ...

  5. NSObject的load和initialize方法(转)

    全文转载自:http://www.cocoachina.com/ios/20150104/10826.html 在Objective-C中,NSObject是根类,而NSObject.h的头文件中前两 ...

  6. load 与initialize的调用顺序小结

    开发中实用方法固然是最贴近应用的,当一些程序原理还是要先搞清晰,根据查找的一些资料,总结了一些load与initialize的调用. APP启动到执行main函数之前,程序就执行了很多代码   执行顺 ...

  7. 细说OC中的load和initialize方法

    OC中有两个特殊的类方法,分别是load和initialize.本文总结一下这两个方法的区别于联系.使用场景和注意事项.Demo可以在我的Github上找到--load和initialize,如果觉得 ...

  8. load vs. initialize

    这篇文章来对比一下NSObject类的两个方法,+load与+initialize. + (void)load; Invoked whenever a class or category is add ...

  9. iOS load和initialize的区别

    可能有些还不清楚load和initialize的区别,下面简单说一下: 首先说一下 + initialize 方法:苹果官方对这个方法有这样的一段描述:这个方法会在 第一次初始化这个类之前 被调用,我 ...

  10. +(void)load; +(void)initialize;有什么用处?

    总得来说: 1.+load方法是在main函数之前调用的: 2.遵从先父类后子类,先本类后列类别的顺序调用: 3.类,父类与分类之间的调用是互不影响的.子类中不需要调用super方法,也不会调用父类的 ...

随机推荐

  1. Nginx之常用基本配置(三)

    前面我们聊了下了Nginx作为WEB服务器对客户端请求相关配置,文件操作优化.Nginx访问控制.basic验证,.状态模块状态页.gzip压缩配置:回顾请参考https://www.cnblogs. ...

  2. appium+python自动化实践之查找元素的等待方式笔记

    元素等待作用 设置元素等待,可以更加灵活的制定等待定位元素的时间,从而增强脚本的健壮性,提高执行效率. 元素等待类型 强制等待:设置固定等待时间,使用sleep()方法即可实现 from time i ...

  3. VMware Workstation 虚拟机的安装及kali Linux 的安装

    要用到虚拟机了,Windows平台的虚拟机现在主要用的是VMWare 和VirtuualBox了.VirtualBox作为一款免费的软件,用着还行.笔者这里主要介绍VMware的安装和使用 . 下载: ...

  4. 使用纯粹的JS构建 Web Component

    原文链接:https://ayushgp.github.io/htm...译者:阿里云 - 也树 Web Component 出现有一阵子了. Google 费了很大力气去推动它更广泛的应用,但是除 ...

  5. 聊聊.netcore采坑那一些事之系统时间and文件路径

    聊聊.netcore采坑那一些事之系统时间and文件路径 Hi,小伙伴大家好,最近工作比较忙,很久没有和大家分享点东西了.这个周末都加了两天班.公司的新项目都是采用.netcore来开发,在开发过程中 ...

  6. SpringBoot1.5.10.RELEASE配置mybatis的逆向工程

    在application.properties配置扫描等,不做多说 1.在pom配置文件中引入mybatis和mysql的依赖,如下: <dependency> <groupId&g ...

  7. iTerm2 都不会用,还敢自称老司机?(上)

    对于需要长期与终端打交道的工程师来说,拥有一款称手的终端管理器是很有必要的,对于 Windows 用户来说,最好的选择是 Xshell,这个大家都没有异议.但对于 MacOS 用户来说,仍然毋庸置疑, ...

  8. linux-TFTP服务

    1.TFTP协议简介TFTP,全称是 Trivial File Transfer Protocol(简单文件传输协议),基于 UDP 实现,该协议简单到只能从远程服务器读取数据或向远程服务器上传数据. ...

  9. opt目录

    在linux环境测试时,会部署到/opt目录下,这是为何呢? 下面来详解Linux的/opt目录: /opt:用户级的程序目录 这里主要存放那些可选的程序. 比如,你想部署firefox测试版,那就装 ...

  10. CSS 权重图

    关系图 图片出处我找不到了. 结论 权重从高到低排序 1. !important 2. style 3. #id 4. .class .child-class 5. .class1.class2 6. ...