前言
- 谈到
NSObject
根类,有朋友可能便会立即想到这两个方法,这两个方法究竟是什么?它们有什么区别,又有什么共同点呢?希望下文能够帮各位朋友重温load
方法与initialize
方法的要点。
+ (void)load;
在运行时,涉及
load
的方法,大概调用过程如下图load
方法在程序中的调用时间段:在App启动加载时调用- 程序依赖的动态链接库加载进内存
- 加载可执行文件的所有符号、代码
- 运行时解析被编译过的符号代码,遍历所有类与分类,按继承关系依次调用类与分类的
load
方法load
方法一般在类/分类添加到运行时环境时调用,一般在application:willFinishLaunchingWithOptions:
调用之前
Xcode - Targets - Build Phases - Complie Sources
中的顺序为默认load
方法的调用顺序,但并非最终的调用顺序,因为被依赖的类会优先调用load
方法。比如子类B是父类A的子类,当把子类B放在Complie Sources
的第一个文件时,编译运行后,会发现首先是父类的load
方法先调用,然后再是子类的load
方法调用,详细调用顺序请见下文load
方法中会自动加锁,保证线程安全调用顺序:依赖的类会先调用,即调用子类时,会先调用父类;分类最后调用,不会覆盖
load
方法- 简析:因为
load
方法在运行时并不是通过消息发送(objc_msgSend
),而是通过函数指针来实现的。所以对于一个有继承关系的类,若子类中的load
方法为空实现,则父类的load
方法除因依赖问题被调用一次外,不会再次被调用。同理,因为不是走消息发送的原因,所以如果在分类中重写load
方法,分类中的load
方法并不会像其他方法一样覆盖原类的load
方法,而是在原类load
方法执行后执行 - 例子A:假设分类B+C是子类B的分类,分类含有
load
方法。而子类B是父类A的子类,在AppDelegate.m
中创建一个子类B对象,则load
方法调用顺序如下:- 调用父类A的
load
方法 - 调用子类B的
load
方法 - 调用分类B+C的
load
方法
- 调用父类A的
- 简析:因为
一般应用场景
- 在
load
方法中实现method_swizzle
- @Glow 技术团队博客 - 瘦身
AppDelegate
- @sunnyxx的技术博客 - 用于判断类/分类是否正确的加载
- 在
+ (void)initialize;
在运行时中,涉及
initialize
的方法,大概调用过程如下图initialize
方法在程序中的调用时间段:第一次主动调用当前类时,即initialize
方法永远是类收到的第一个消息。注意,上文提及的load
方法因为是通过函数指针调用,并不是走消息发送的途径,所以尽管load
方法在initialize
方法调用前调用,但load
方法依然不能称作第一个消息initialize
方法在线程安全的环境中执行,即只有执行initialize
的那个线程可以操作类/类实例,其他线程都会被阻塞,等待initialize
方法执行完调用顺序:依赖的类会先调用,即调用子类时,会先调用父类;当子类空实现时,会再次调用父类方法;分类会覆盖
load
方法简析:因为
initialize
方法在运行时是通过消息发送(objc_msgSend
)来实现的。所以对于一个有继承关系的类,如果子类中的initialize
方法为空实现,则父类的initialize
方法会被自动调用(等价于我们系统帮我们默认在空实现中写了[super initialize]
)。同理,因为消息发送的原因,所以子类分类如果分类中重写initialize
方法,分类中的initialize
方法会像其他方法一样覆盖子类的initialize
方法。例子B:假设子类B是父类A的子类,B中
initialize
方法为空实现,在AppDelegate.m
中创建一个子类B对象,则initialize
方法调用顺序如下:- 调用父类A的
initialize
方法 - 再次调用父类A的
initialize
方法 - 调用子类B自身的
initialize
方法
- 调用父类A的
例子B补充:由于在父类A中,
initialize
方法会被调用两次,可在父类A使用以下代码,避免父类A的initialize
方法多次执行初始化代码:例子C:假设分类B+C是子类B的分类,分类含有
initialize
方法,但该方法不含[super initialize]
。而子类B是父类A的子类,子类B中initialize
方法为非空实现,在AppDelegate.m
中创建一个子类B对象,则initialize
方法调用顺序如下:- 调用父类A的
initialize
方法 - 调用分类B+C的
initialize
方法(注意,由于我们没写[super initialize]
,所以仅调用一次父类initialize
方法,系统不会自动为我们增加该语句)
- 调用父类A的
一般应用场景
用于初始化无法直接初始化的全局状态的
static
变量,如下,NSArray
无法直接初始化,只能在initialize
方法中对其进行初始化
对比表格
对比点 | load 方法 |
initialize 方法 |
---|---|---|
是否所有类都会调用 | 是 | 否 |
什么情况下会被调用 | 只要类所在的文件夹被引用 | 只要类的方法被调用 |
什么时候调用 | 被添加到运行时环境时 | 收到消息时/永远不调用 |
在main 方法前/后调用 |
前 | 后 |
调用时运行时环境 | 脆弱的运行时环境,自动释放池尚未建立 | 完整的运行时环境 |
调用本质 | 函数指针调用 | 消息发送(objc_msgSend ) |
系统是否最多调用一次 | 是 | 否,父类的可能被执行多次 |
空实现时,自动调用父类实现 | 否 | 是 |
父类、子类、分类的调用顺序 | 父类 -> 子类 -> 分类 | 父类 -> 分类/子类 |
是否线程安全 | 是 | 是 |
禁忌点
load
方法- 不要在
load
方法中滥用其他类- 这是因为我们无法判断各个类的载入顺序,有可能会导致依赖环
- 不要在
load
方法中滥用需要Autoreleasepool
的方法,如[NSArray array]
等方法- 这是因为此时运行时环境脆弱,
Autoreleasepool
尚未加载,如果确实要使用,需使用@Autoreleasepool
代码块将其包围
- 这是因为此时运行时环境脆弱,
- 不要在
load
方法中包含复杂的代码,即使用时不能重度依赖该方法- 这是因为过于复杂的
load
方法会阻塞线程,导致程序无响应
- 这是因为过于复杂的
- 不要在
load
方法中包含等待锁/调用可能会加锁的方法- 这是因为我们无法判断各个类的载入顺序,有可能会导致死锁/饥饿
- 不要在
initialize
方法- 绝不应该通过代码直接调用
initialize
方法- 这是因为我们应该保证
initialize
方法由系统调用,保证其仅调用一次
- 这是因为我们应该保证
- 不要在
initialize
方法中包含复杂的代码,即使用时不能重度依赖该方法- 这是因为其有可能阻塞UI线程
- 不要在
initialize
方法中滥用其他类- 这是因为在本类还没有完全初始化的时候,却又被自身调用的类调用了,即有可能会导致依赖环
- 绝不应该通过代码直接调用
总结
load
方法与initialize
方法可以main
方法为分界线,将调用顺序划分如下图,注意,其中initialize
方法也有可能不调用load
在运行时通过函数指针调用,initialize
方法通过消息发送调用,这直接导致了他们之间的差异
- 本文用到的代码下载
- Github Repo - DeepingLoadAndInitialize
- Github Repo - ObjC-Runtime by @RetVal
- 运行环境:Xcode 8.2.1(8C1002)
- Last Edited:2016.1.17
- Author:@Seahub
- Please contact me if you want to share this Article, 3Q~