load 和 initialize 方法的执行顺序以及类和对象的关系

先了解一下应用启动之后,做了什么。
main.m 中的 main() 是程序的入口,但在进入 main 函数之前,程序就执行了很多代码(不然也不会启动那么久)。
启动后执行顺序:
将程序依赖的动态链接库加载进内存
加载可执行文件中的所有符号、代码
runtime 解析被编译过的符号代码,遍历所有 Class,按继承层级依次调用Class 的 load 方法和其 Category 的 load 方法。

load 方法的执行顺序

首先来做点测试,来看看 load 方法的执行顺序。
先建一个 Single View Application。展开 Build Phases 的 Compile Sources,如下图:


屏幕快照 2016-05-10 13.02.51.png

在每个类的 @implementation 里加上

+ (void)load {
    NSLog(@"%s", __func__);
}

来看看每个类的 load 方法的调用顺序(main.m 我做了特殊处理)。以下是运行结果:

    JMTestDemo[14939:1769791] +[ViewController load]
    JMTestDemo[14939:1769791] +[AppDelegate load]
    JMTestDemo[14939:1769791] +[ClassMain load]

顺序和 Compile Sources 顺序一致,目前来看,Compile Sources 的顺序就是 load 方法的调用顺序。

再来一个测试,依次添加 ClassFather,以及它的子类 ClssSon,以及另一个 ClassA。结果 Compile Sources 的顺序和我想象中的不一样,如下图


屏幕快照 2016-05-10 13.10.18.png

看起来毫无规律啊。(这是一个问题,先记录一下)

这个时候我改变顺序,将 ClassSon 移动到最前面,ClassFather 移动到最后面。如图


屏幕快照 2016-05-10 13.49.31.png

下面是运行结果:

    JMTestDemo[15034:1788736] +[ClassFather load]
    JMTestDemo[15034:1788736] +[ClassSon load]
    JMTestDemo[15034:1788736] +[ViewController load]
    JMTestDemo[15034:1788736] +[AppDelegate load]
    JMTestDemo[15034:1788736] +[ClassA load]
    JMTestDemo[15034:1788736] +[ClassMain load]

也就是本应先执行 ClassSon load 方法,但先执行了 ClassFather load 方法,再来一次测试。
我将 ClassSon 里的 load 方法注释掉,以下是运行结果:

    JMTestDemo[15055:1791953] +[ViewController load]
    JMTestDemo[15055:1791953] +[AppDelegate load]
    JMTestDemo[15055:1791953] +[ClassA load]
    JMTestDemo[15055:1791953] +[ClassMain load]
    JMTestDemo[15055:1791953] +[ClassFather load]

也就是说,只有在你重写了 load 方法的时候,会在执行你的 load 之前,当父类未加载时,先执行父类的 load 方法。

再来一个分类的情况,分类就很有意思了。先添加了 ClssSon+category,ClssSon+category2,ClassFather+category。将它们移动至最上方。如图

屏幕快照 2016-05-10 14.10.56.png

运行结果

    JMTestDemo[15181:1808284] +[ClassFather load]
    JMTestDemo[15181:1808284] +[ClassSon load]
    JMTestDemo[15181:1808284] +[ViewController load]
    JMTestDemo[15181:1808284] +[AppDelegate load]
    JMTestDemo[15181:1808284] +[ClassA load]
    JMTestDemo[15181:1808284] +[ClassMain load]
    JMTestDemo[15181:1808284] +[ClassSon(category2) load]
    JMTestDemo[15181:1808284] +[ClassFather(category) load]
    JMTestDemo[15181:1808284] +[ClassSon(category) load]

明明应该最早执行 load 方法的分类,却统统最后执行,甚至晚于和它没有啥关系的 ClassMain load。所以分类低人一等,最晚执行 load 方法。

得出结论,load 的执行顺序满足以下几条

  • 执行子类的 load 之前,当父类未加载时,先执行父类的 load 方法。
  • 分类的 load 方法统一在最后执行
  • 优先满足以上两条,再满足按 Compile Sources 的顺序执行 load 方法。

initialize 方法的执行顺序

这个时候来测试 initialize 的执行顺序,在 ClassFather ClassSon 以及他们的分类都重写 initialize 方法,并将 ClassFather 移动至 ClassSon 的前面,在 ClassFather load 里添加调用 ClassSon 的类方法的代码,如下

+ (void)load {
    NSLog(@"%s", __func__); 
    [ClassSon method];
}

运行结果如下

    JMTestDemo[15325:1836741] +[ClassFather load]
    JMTestDemo[15325:1836741] +[ClassFather(category) initialize]
    JMTestDemo[15325:1836741] +[ClassSon(category) initialize]
    JMTestDemo[15325:1836741] +[ClassSon method]
    JMTestDemo[15325:1836741] +[ClassSon load]
    JMTestDemo[15325:1836741] +[ViewController load]
    JMTestDemo[15325:1836741] +[AppDelegate load]
    JMTestDemo[15325:1836741] +[ClassA load]
    JMTestDemo[15325:1836741] +[ClassMain load]
    JMTestDemo[15325:1836741] +[ClassSon(category2) load]
    JMTestDemo[15325:1836741] +[ClassSon(category) load]
    JMTestDemo[15325:1836741] +[ClassFather(category) load]

从运行结果来看,先执行了 ClassFather(category) initialize,再执行了 ClassSon(category) initialize,而 ClassSon load 在后面执行。
也就是说 load 方法还未执行也不会影响到这个类的使用。
另一个现象是执行子类 initialize 的时候会先执行其父类的 initialize。且 category 的覆写效应对 load 方法无效,但对 initialize 方法有效。且按 Complile Sources 的顺序,ClassSon(category2) 先覆写了 ClassSon 的 initialize 方法,接着 ClassSon(category) 覆写了 ClassSon(category2) 的 initialize。

如果将子类以及类别的 initialize 注释掉,再修改 ClassFather(category) initialize ,如下

+ (void)initialize {
    NSLog(@"调用者:%@ 调用方法:%s",NSStringFromClass(self), __func__);
}

结果如下

    JMTestDemo[15458:1863222] +[ClassFather load]
    JMTestDemo[15458:1863222] 调用者:ClassFather 调用方法:+[ClassFather(category) initialize]
    JMTestDemo[15458:1863222] 调用者:ClassSon 调用方法:+[ClassFather(category) initialize]
    JMTestDemo[15458:1863222] +[ClassSon method]

也就是子类会继承父类的 initialize 。当执行完父类的 initialize 方法,准备执行子类的 initialize 方法时,会根据继承链找到父类的 initialize 执行。为了防止重复执行 initialize 里的代码,可以根据调用者来决定是否执行 initialize 里的其它代码。

类和对象

这块写了一部分,但查资料的时候查到写的非常不错的,我觉得我没有写的必要了,留下链接,强烈建议想对 iOS 开发中的类和对象有更深了解的人看看。

还是自己写写掌握的更多点,写的有点乱,边学边写把,以后再整理。

对象是在 iOS 开发中最常用的东西,也是最多的东西,一个对象它会有自己的属性,自己的行为,想像中一个对象所占用的空间会很大,但其实一个对象所占的空间是非常少的,少到只存储了必要的数据。isa_t 指针(虽然它已经并不单纯是一个指针,但本文还是以它的主要用途来称呼它)和这个对象的成员变量的值。

比如一个 Student 类型对象,只有 height 和 weight 两个 NSInteger 类型成员变量。那么 Student 类型对象 jim 所占内存空间只需要 24 bytes (64 位机型 NSInteger 以及 isa_t 指针都是 8 bytes)。不过考虑到 CPU 的存取原理,不内存对齐一下 CPU 的内存开销会很大,所以这样一个 jim 对象只会占用 32 bytes。(有关内存对齐可以阅读我同学写的博客了解一下)

你平时操作的对象就只占这么点内存,成员变量的存储位置按顺序跟在 isa_t 指针后面。想知道 height 的值要放在哪个位置,那么 isa_t 的作用就体现出来了,isa_t 指针现在已经不单纯是个指针,isa_t 是一个 union 类型的结构体,之所以会从 isa 指针改成 isa_t 结构体,也是因为在 64 位的 CPU 上,使用整个指针大小的内存来存储 isa 指针有些浪费,在 ARM64 运行的 iOS 只使用了 33 位作为指针(33 位够指向 8GB 内存地址,近期内看是远远够用的,何况指向的是虚拟内存地址,一个 APP 想申请 8GB 以上的内存空间,这是要上天吗?),而剩下的 31 位用于其它目的。而 33 位的指针指向了这个对象对应的类对象 Student,而储存类对象是一个 obj_class 的结构体,里面含有一个 class_data_bits_t 结构体,里面只存放着一个 64 位的 uintptr_t 结构体 用于存储与类有关的信息,和 isa_t 结构体类似。

在编译期间,其中的 33 位作为一个指针指向 class_ro_t 结构体,class_ro_t 结构体主要用于存储当前类在编译期就已经确定的属性、方法以及遵循的协议。如下图

图片来源:Draveness

而在运行的时候由 runtime 将类对象的结构变成下图。

图片来源:Draveness

class_rw_t 结构体存储着指向只读区域 class_ro_t 结构体的 ro 指针,
以及将类自己实现的方法(包括分类)、属性和遵循的协议加载到 methods、 properties 和 protocols 列表中。
而 class_rw_t 的结构体里 methods 指向的是方法链表。runtime 加载分类的时候,会将分类里的方法从 methods 的头部开始添加。而发送消息的时候,在类里找方法也是从 methods 头部开始寻找,找到了就停止,所以分类的方法会”覆盖”本来类的同名方法。晚加载的分类也会”覆盖”早加载的分类的方法。
既然是链表,就意味着每次查找方法都会是比较耗时的操作,所以才会在 obj_class 结构体里有一个缓存已经调用过的方法的 cache_t 结构体。

cache_t 结构体里面有个指针指向一个散列表,用来以选择子作为 key,函数地址作为 value。查找方法对应的函数地址时,会先从这个散列表里查询有没有缓存,没有缓存命中的话才会去 methods 里查找方法。而之所以 methods 使用链表而不是更快的散列表是因为类对象的方法列表需要可以动态添加的。找不到的话就得去找爸爸帮忙了。爸爸,爸爸,这个行为我不会,你会的话帮我执行这个行为。爸爸也先找自己的缓存表,没有就在 methods 里找,也不会就找爷爷,爷爷也不会的话,看他还有在世的爸爸不,没有的话 看看方法接收类能不能动态添加这个行为,也就是现学,当然它的爸爸或者爷爷现学也行,都太懒不想学就吼一句,隔壁老王老李老赵你们有人会吗?有人会的话就将事情扔给他们,这就是方法转发。如果没人愿意的话那就不执行这个行为了,丢弃。有一点就是如果缓存中的内容大于容量的 3/4 就会扩充缓存,(为什么不是满了的时候而是 3/4 呢,毕竟是散列表,太满的话查询和插入数据的效率都会变低)使缓存的大小翻倍。但在缓存翻倍的过程中,当前类全部的缓存都会被清空,Objective-C 出于性能的考虑不会将原有缓存拷贝到新初始化的内存中。

再来看看 objc_ivar_list 结构体,里面有 objc_ivar 结构体数组,记录着每个成员变量的,名字,类型,偏移量。在新建一个对象的时候,就会根据名字,类型,偏移量来设计这个对象的内存结构,设计跟在 isa_t 结构体地址后面的成员变量的位置。

typedef struct objc_ivar_list {  
    int   ivar_count;                               
    struct objc_ivar {  
        const char* ivar_name;                      
        const char* ivar_type;                        
        int        ivar_offset;                             
    } ivar_list[1];                                
} IvarList, *IvarList_t;  

类的方法和属性都是可以动态添加的,但是成员变量不行,是因为当一个对象生成之后,会为它分配一块固定大小的空间,但是你添加了一个实例变量就相当于增加了需要的空间。成员变量是靠与对象地址的偏移量来确定地址的,就和一个声明了固定空间的数组一样,无法再在后面申请新的空间存放数据(因为这些内存可能有其它的对象在使用)。

未完待续(也可能太监)

参考资料

iOS程序main函数之前发生了什么
从 NSObject 的初始化了解 isa
深入解析 ObjC 中方法的结构

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • Objective C类方法load和initialize的区别过去两个星期里,为了完成一个工作,接触到了NSOb...
    亦晴工作室阅读 1,302评论 0 10
  • iOS开发中总能看到+load和+initialize的身影,网上对于这两个方法有很多解释,官方也有说明,但有些细...
    朱晓辉阅读 27,399评论 19 139
  • 一、Runtime简介 Runtime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消...
    林安530阅读 1,056评论 0 2
  • 我是苍茫东海之上一个随浪浮沉的葫芦。我硕大无比。我曾经长在东海之滨的一根仙藤之上,也曾经被一个农夫摘回家去,我被...
    凤兮凤兮阅读 270评论 1 1