这里所要介绍的 load 与 initialize 方法,这两个是类方法,是系统的方法。我曾经见过有人在自定义的class中写了一个 initialize对象 方法,无知的我在那里绕了半天。
建议大家,从这里下载我的样例项目。
一定要记得下载loadAndinitialize。
一、先抛出问题
- 1、这两个方法的调用时机
- 2、有何异同
- 3、有何用途
二、load
在文件刚开始装载的时候被调用(在main方法被调用之前),并有且仅调用一次。
运行项目Load,打印结果如下:
2017-06-04 23:20:11.855869+0800 Load[7892:2777060] OtherObject
2017-06-04 23:20:11.856272+0800 Load[7892:2777060] ParentObject
2017-06-04 23:20:11.856298+0800 Load[7892:2777060] ChildObject
2017-06-04 23:20:11.856321+0800 Load[7892:2777060] ChildObject+Category
load 的执行顺序
- 1、相关联的class的执行顺序: 父类 --> 子类 --> 分类。
- 2、不相关联执行顺序:是根据 项目配置中的 Compile Sources 顺序决定的。(调换项目Load中的OtherObject的位置可以看看效果)
各位大神的建议
在 load类方法 中,尽量简洁,不要写复杂的逻辑。一般可以将 Swizzled 方法在 load类方法 中实现。
三、initialize
在当前class第一次被用到的时候被调用(第一次发送消息),同样有且仅调用一次。所以,也有很多大神说这个方法有懒加载的感觉,用到才会触发。不像 load类方法 那样,一旦装载就赶紧触发。
情景一
打开 项目initialize 直接运行,会发现没有打印日志。说明在没有给当前class发送消息,是不会触发 initialize类方法 的。
情景二
在 ViewController.m 文件中仅打开 test1 方法,运行项目,打印日志如下:
2017-06-05 00:15:06.312925+0800 Initialize[7959:2792250] ParentObject = ParentObject
2017-06-05 00:15:06.313506+0800 Initialize[7959:2792250] ChildObject = ChildObject
情景三
在 ViewController.m 文件中仅打开 test1 方法的前提下,将 ChildObject 中的 initialize类方法 注释掉,运行项目,打印日志如下:
2017-06-05 00:16:17.099422+0800 Initialize[7963:2793122] ParentObject = ParentObject
2017-06-05 00:16:17.099534+0800 Initialize[7963:2793122] ParentObject = ChildObject
情景二 与 情景三 同时说明: 父类中的 initialize类方法 会被子类触发。在子类中即使没有实现 initialize类方法 ,也会默认的去调用父类的 initialize类方法 。所以在实现 +initialize 方法的时候一定要做好判断,放在当前 Class 被继承导致一些重复的操作(在代码中已经做了判断)。
在进行之后的情景之前,记得将 ChildObject 中的 initialize类方法 打开。
情景四
在 ViewController.m 文件中仅打开 test2 方法,运行项目,打印日志如下:
2017-06-05 00:27:31.883299+0800 Initialize[7968:2795570] ParentObject = ParentObject
2017-06-05 00:27:31.883410+0800 Initialize[7968:2795570] ChildObject = ChildObject
2017-06-05 00:27:31.883463+0800 Initialize[7968:2795570] ------华丽的分割线------
情景五
在 ViewController.m 文件中仅打开 test3 方法,运行项目,打印日志如下:
2017-06-05 00:29:47.599994+0800 Initialize[7971:2796349] ParentObject = ParentObject
2017-06-05 00:29:47.600114+0800 Initialize[7971:2796349] ------华丽的分割线------
2017-06-05 00:29:47.600160+0800 Initialize[7971:2796349] ChildObject = ChildObject
情景四 与 情景五 同时说明: initialize类方法 有且仅调用一次。
细心的大神,应该发现了点漏洞了。会问 情景三 又是怎么回事? 在 ParentObject 类中明明被触发了两次。再仔细一看,其实有一个是子类触发的,看 self 的打印值是 ChildObject 就明白了。那一般应该怎么处理这种情况呢?解决方案是,在 ParentObject 类中的 initialize类方法 中作这样的判断即可:
+ (void)initialize {
NSLog(@"ParentObject = %@", self);
if (self == [ParentObject class]) {
// TODO: 做自己喜欢做的事
}
}
在 initialize类方法 中,一般都是作一些全局性的一次性设置。比如 UITabBarController 与 UINavigationController 全局的统一设置。
比如,我常用的:
+ (void)initialize
{
UINavigationBar *navigationBar = [UINavigationBar appearance];
[navigationBar setBackgroundImage:[UIImage imageWithColor:GlobalColor] forBarMetrics:UIBarMetricsDefault];
if ([UINavigationBar instancesRespondToSelector:@selector(setShadowImage:)]) {
[navigationBar setShadowImage:[UIImage imageWithColor:[UIColor clearColor]]];
}
// 统一修改控制器title的字体颜色
NSMutableDictionary* dictM = [NSMutableDictionary dictionary];
dictM[NSFontAttributeName] = Font(19);
dictM[NSForegroundColorAttributeName] = [UIColor whiteColor];
[navigationBar setTitleTextAttributes:dictM];
}
四、load 与 initialize 同时出现
打开 项目 02LoadandInitialize, 直接运行,打印日志如下:
2017-06-05 00:55:47.278158+0800 02LoadandInitialize[7976:2801773] initialize = LoadandInitializeObject
2017-06-05 00:55:47.278589+0800 02LoadandInitialize[7976:2801773] load = LoadandInitializeObject
两个问题来了:
- 1、没有在任何地方用到 LoadandInitializeObject ,initialize类方法 尽然被触发了,这是为什么?
- 2、仔细一看,initialize类方法 尽然在 load类方法 前面执行了,这又是为什么?
其实,在 项目 02LoadandInitialize 中就能找到答案。如果找不到原因,请直接打开 项目 03LoadandInitialize 运行一下,打印日志如下:
2017-06-05 01:04:30.237773+0800 03LoadandInitialize[7979:2804252] load 开始执行
2017-06-05 01:04:30.238219+0800 03LoadandInitialize[7979:2804252] initialize = LoadandInitializeObject
2017-06-05 01:04:30.238271+0800 03LoadandInitialize[7979:2804252] load = LoadandInitializeObject
2017-06-05 01:04:30.238348+0800 03LoadandInitialize[7979:2804252] load 执行即将结束
终于真相大白了,原来是在 load 方法中调用了[self class] 导致的。所以一般是强烈不推荐在 +load 方法中调用当前 Class 与其他 Class 的任何方法的。如果在当前的 Class 的 +load 中调用其它 Class 的方法,一旦出现问题,是很难排查的。
六、一个小总结
- 1、load 与 initialize 都是系统自动调用的,不要手动调用。
- 2、尽量不要手动的通过 super 在子类中调用父类的方法。
- 3、两个方法,有且仅会触发一次。 只是要注意 initialize 的 情景三 特例。
- 4、load 优先于 initialize 触发。
七、分类中重写 +load 与 +initialize
- 1. +load 方法不管在原生 Class 中还是分类中,一旦重写了都会被调用。
- 2. +initialize 方法如果在分类中重写了,那么原生 Class 中的就不会被调用。
具体的原因是两个方法的调用逻辑不一样。
+load 方法是在 dyld 加载的过程中被调用,调用的逻辑是一旦检测到+load 方法被重写,那么直接就调用。
+initialize 方法的调用逻辑与通常的消息发送机制一致,都是通过当前 Class 的 isa 来进行查找方法调用。其中 Class 的 isa 的值就是其对应的元类(meta-class)。
为什么分类中重写了原生 Class 中的方法之后,原生 Class 中的方法失效?
对于一个 Class 来说,所有的分类方法都被放于原生 Class 方法的前面。可以看看下面这张图。
所以一旦分类中重写了原生类中的方法,那么原生类中的方法就永远没有机会被调用。
八、源码分析
接下来直接从源码的角度去分析这两个方法的调用原理,当前分析的源码是当前的最新版本:objc4-750.tar.gz
8.1 load
+load 方法在什么时机调用的?
将代码定位到 objc-os.mm 文件的 void _objc_init(void) 函数:
从注释中可以了解到这个函数是在 dyld 启动时对项目的一个初始化入口。接下来先看 load_images。
会经过如下步骤(括号中的数字为代码的行号):
load_images(888) ---> call_load_methods(2190)
其实这个函数的注释也比较金典,在这里就不贴出来了。主要是这个函数的内容,可以看出先调用类中的 load 方法,在调用分类中的,在这里就能证明了。
进入 call_class_loads 中看看:
再看看 struct loadable_class 是个啥:
上面还包括 分类的。
终于明白了,load 方法的调用,是直接取出内存地址直接调用的。
回到上一步,再进入 call_category_loads(375):
直接调用。
load 小节
终于明白了为什么 load 方法,不管是在 Class 中,还是在分类中都能正常的调用。原来是因为在加载的时候,直接通过 method 直接调用的。
8.2 initialize
关于 initialize 就有点不一样了,这个方法是在当前 Class 第一次发送消息的时候才会调用的。我们都清楚,发送消息的核心函数是objc_msgSend,那就来研究一下这个函数,但是这个函数几乎都是汇编实现,我们可以找到另一个入口函数 class_getInstanceMethod 具体的步骤如下:
objc-runtime-new.mm ----> class_getInstanceMethod ----> lookUpImpOrNil(4804) ----> lookUpImpOrForward(4989) -----> _class_initialize(4892) ----> callInitialize(537)
终于找到了这里:
我的神,原来如此。这里使用的是 objc_msgSend,这下子也终于明白了为啥在 分类中 重写 + initialize 之后, 原类中的方法就 失效 了。
initialize 小节
+ initialize 是通过 objc_msgSend 的机制被调用的。
8.3 load 与 initialize 小节
在系统自动调用的情况下:
- 1、load是直接通过方法地址调用的,所以只要是重写的,不管在哪里都能调用。
- 2、initialize 在调用的时候,使用的是 objc_msgSend 机制。
以上的小节仅仅是 在系统调用 的情况,如果那个调用手动的调用 load 的话,肯定是按照 objc_msgSend 机制 的逻辑执行的。
就、 分类与原类中的方法布局
其实在上面已经通过这张图片进行了介绍:
接下来看一下源码,通过源码来证明这一结论。
将代码定位到 objc-os.mm 文件的 void _objc_init(void) 函数:
接下来先看 map_images。
会经过如下步骤(括号中的数字为代码的行号):
map_images(888) ---> map_images_nolock(2162) ----> _read_images(577) ---->remethodizeClass(2735) -----> attachCategories(916)
其实进入 attachCategories 函数,有几个事情想要先说一下。
好像都在处理三个东西:
- 1、方法列表(mlists)
- 2、属性列表(proplists)
- 3、协议列表(protolists)
这也对,毕竟在分类中主要也就是这三个东西。当然了,在分类中也能动态的添加成员变量,但是这样的成员变量是模仿的,并不是真正的成员变量。在实际上是被添加到了一个全局的 AssociationsManager 中的。
接下来的重点是看下面的函数中到底做了什么操作:
主要就是这一坨:
红框框中的逻辑主要有:
- 1、通过 realloc 函数重新分配内容,主要目的是扩容(newCount)
- 2、将原类中的数据往后移动
- 3、将分类中的数据分到前面
厉害了,怪在分类中重写了原类中的方法之后就掉不到原类中的方法了,原来是因为分类中的方法放到了前面,所以在方法查找的过程中,只要是找到了就不会再继续找了。但是找到的却是分类中的方法。
在这里也清楚了一件事情,如果分类中重写了原类中的方法,那么原类中的方法还是存在的,只是因为地址被放到最后了。那么问题来了,如何调用原类中的方法呢?这个就比较简单了,直接遍历查找,最后找到的那个就是原类中的方法了。
至此,对 load 与 initialize 方法 的分析就差不多了。