OC 中的 load 与 initialize 方法

这里所要介绍的 loadinitialize 方法,这两个是类方法,是系统的方法。我曾经见过有人在自定义的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类方法 中,一般都是作一些全局性的一次性设置。比如 UITabBarControllerUINavigationController 全局的统一设置。

比如,我常用的:

+ (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、没有在任何地方用到 LoadandInitializeObjectinitialize类方法 尽然被触发了,这是为什么?
  • 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 方法的前面。可以看看下面这张图。

image.png

所以一旦分类中重写了原生类中的方法,那么原生类中的方法就永远没有机会被调用。

八、源码分析

接下来直接从源码的角度去分析这两个方法的调用原理,当前分析的源码是当前的最新版本:objc4-750.tar.gz

8.1 load

+load 方法在什么时机调用的?

将代码定位到 objc-os.mm 文件的 void _objc_init(void) 函数:

image.png

从注释中可以了解到这个函数是在 dyld 启动时对项目的一个初始化入口。接下来先看 load_images
会经过如下步骤(括号中的数字为代码的行号):

load_images(888) ---> call_load_methods(2190)

image.png

其实这个函数的注释也比较金典,在这里就不贴出来了。主要是这个函数的内容,可以看出先调用类中的 load 方法,在调用分类中的,在这里就能证明了。

进入 call_class_loads 中看看:

image.png

再看看 struct loadable_class 是个啥:

image.png

上面还包括 分类的。
终于明白了,load 方法的调用,是直接取出内存地址直接调用的。

回到上一步,再进入 call_category_loads(375):

image.png

直接调用。

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)

终于找到了这里:


image.png

我的神,原来如此。这里使用的是 objc_msgSend,这下子也终于明白了为啥在 分类中 重写 + initialize 之后, 原类中的方法就 失效 了。

initialize 小节

+ initialize 是通过 objc_msgSend 的机制被调用的。

8.3 load 与 initialize 小节

在系统自动调用的情况下:

  • 1、load是直接通过方法地址调用的,所以只要是重写的,不管在哪里都能调用。
  • 2、initialize 在调用的时候,使用的是 objc_msgSend 机制。

以上的小节仅仅是 在系统调用 的情况,如果那个调用手动的调用 load 的话,肯定是按照 objc_msgSend 机制 的逻辑执行的。

就、 分类与原类中的方法布局

其实在上面已经通过这张图片进行了介绍:


image.png

接下来看一下源码,通过源码来证明这一结论。

将代码定位到 objc-os.mm 文件的 void _objc_init(void) 函数:

image.png

接下来先看 map_images
会经过如下步骤(括号中的数字为代码的行号):

map_images(888) ---> map_images_nolock(2162) ----> _read_images(577) ---->remethodizeClass(2735) -----> attachCategories(916)

其实进入 attachCategories 函数,有几个事情想要先说一下。


image.png

好像都在处理三个东西:

  • 1、方法列表(mlists)
  • 2、属性列表(proplists)
  • 3、协议列表(protolists)

这也对,毕竟在分类中主要也就是这三个东西。当然了,在分类中也能动态的添加成员变量,但是这样的成员变量是模仿的,并不是真正的成员变量。在实际上是被添加到了一个全局的 AssociationsManager 中的。

接下来的重点是看下面的函数中到底做了什么操作:


image.png

主要就是这一坨:


image.png

红框框中的逻辑主要有:

  • 1、通过 realloc 函数重新分配内容,主要目的是扩容(newCount)
  • 2、将原类中的数据往后移动
  • 3、将分类中的数据分到前面

厉害了,怪在分类中重写了原类中的方法之后就掉不到原类中的方法了,原来是因为分类中的方法放到了前面,所以在方法查找的过程中,只要是找到了就不会再继续找了。但是找到的却是分类中的方法。

在这里也清楚了一件事情,如果分类中重写了原类中的方法,那么原类中的方法还是存在的,只是因为地址被放到最后了。那么问题来了,如何调用原类中的方法呢?这个就比较简单了,直接遍历查找,最后找到的那个就是原类中的方法了。

至此,对 load 与 initialize 方法 的分析就差不多了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容