+load VS +initialize
调用时机
+load
通过上一篇 从 MachO 加载到对象创建! 可以了解到:
- DYLD 在初始化主程序时发起
load_images
回调, 从而调用所有类以及分类的+load
方法 (当然, 还有属性 / 协议, 依次是 方法-属性-协议-分类).
+initialize
- 新建类
WXHuman
并重写+initialize
方法; - 因为
+initialize
是类方法, 调用的时候是通过objc_msgSend
调用, 所以在新建类且没有对该类作任何方法调用的时候, 此时+initialize
不会调用 ! - 在调用
[WXHuman alloc]
时, runtime 通过objc_msgSend
给 Person 类对象发送 alloc 消息, 当断点调试+initialize
方法时,可以看到调用栈:
![](https://img.haomeiwen.com/i14617896/b419f589a7a8c7d1.png)
- 图注 : 1.
_class_initialize(Class)
调用了两次是因为在main()
函数中创建的对象是子类对象WXPerson
而不是WXHuman
.
2._CALLING_SOME_+initialize_METHOD
方法仅仅是为了在调用堆栈中方便跟踪提供的消息.
因为方法调用会转换为消息发送, 所以会看到在发送消息的时候调用了 _objc_msgSend_uncached
函数.
那么这个方法又是怎么被调起的?
注: 因为消息发送会反复被调用, 考虑性能所以苹果用汇编实现了 objc_msg 机制
.
所以 :
_objc_msgSend
汇编片段 (取自 objc-msg-arm.s
, 感觉好理解些, 大同小异) :
ENTRY _objc_msgSend // 入口
cbz r0, LNilReceiver_f // **获取方法调用者
ldr r9, [r0] // r9 = self->isa
GetClassFromIsa // r9 = class -- **line 307 - .macro GetClassFromIsa
CacheLookup NORMAL // line 251 - .macro CacheLookup **缓存查找
// cache hit, IMP in r12, eq already set for nonstret forwarding
bx r12 // call imp -- **缓存找到并执行
// 没有找到缓存, 则会跳转到 __objc_msgSend_uncached
CacheLookup2 NORMAL
// cache miss
ldr r9, [r0] // r9 = self->isa
GetClassFromIsa // r9 = class
b __objc_msgSend_uncached // line 691 -- STATIC_ENTRY __objc_msgSend_uncached
// Receiver 为 nil
LNilReceiver:
// r0 is already zero
mov r1, #0
mov r2, #0
mov r3, #0
FP_RETURN_ZERO // 返回 nil
bx lr // 跳转原执行入口
END_ENTRY _objc_msgSend // 结束
- 大概意思:
.获取方法调用者
.获取方法调用者 isa
.从缓存查找方法
.缓存找到就执行 imp
.没有找到就会调用__objc_msgSend_uncached
.返回 nil - 在执行
__objc_msgSend_uncached
时, 又会调用MethodTableLookup
函数
STATIC_ENTRY __objc_msgSend_uncached
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band r9 is the class to search
// line 649 .macro MethodTableLookup 查找方法列表
// returns IMP in r12
MethodTableLookup NORMAL
bx r12
END_ENTRY __objc_msgSend_uncached
- 再看
MethodTableLookup
:
.macro MethodTableLookup
stmfd sp!, {r0-r3,r7,lr}
add r7, sp, #16
sub sp, #8 // align stack
FP_SAVE
.if $0 == NORMAL
// receiver already in r0
// selector already in r1
.else
mov r0, r1 // receiver
mov r1, r2 // selector
.endif
mov r2, r9 // class to search
blx __class_lookupMethodAndLoadCache3
mov r12, r0 // r12 = IMP
.if $0 == NORMAL
cmp r12, r12 // set eq for nonstret forwarding
.else
tst r12, r12 // set ne for stret forwarding
.endif
FP_RESTORE
add sp, #8 // align stack
ldmfd sp!, {r0-r3,r7,lr}
.endmacro
通过函数调用栈可以看到汇编最终调用到 _class_lookupMethodAndLoadCache3
函数 (objc-msg-x86_64.s
有提示), 定义为 message dispatcher
(消息调度者), 由于类也可以当做对象, 所以在这个方法中需要通过条件断点进行调试.
最终该方法中又会调用:
lookUpImpOrForward(cls,
sel,
obj,
YES/*initialize*/,
NO/*cache*/,
YES/*resolver*/);
该函数大致实现:
-
cache标记缓存查找(当前为 NO)
-
保证类实现:
if (!cls->isRealized()) { realizeClass(cls); }
-
然后经过
!cls->isInitialized()
判断 (initialize 传入为 YES),
如果 cls(不是元类) 没有初始化, 则会进行类初始化_class_initialize
, 该类中首先会一直查找其父类(只会实现父类) -
然后经过一系列判断后便会调用
callInitialize
==>((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize)
.
至此, 通过 objc_msgSend 给 cls 发送 initialize 消息, 调用完毕!
小结 :
+load
方法在程序加载时已经执行, 比 main()
函数要早.
+initialize
(官方解释: Initializes the class before it receives its first message.), 显然决定 +initialize
方法调用时机是在第一次调用 cls 的方法前;
注意 : 像 NSObject (不能重载 +initialize
), NSThread 这样的类在 main()
函数前就会调用其方法, 所以调用 +initialize
时机也会在 main()
函数之前, 甚至在 +load
前调用!
所以对于自定义类 +load
比 +initialize
要早很多, 但是有些系统类的 +initialize
方法会在 +load
之前调用!
另 : 系统调用 +load
方法不会触发 +initialize
方法, 而手动调用 +load
方法时会触发 +initialize
方法.
eg: 在 +load
方法里面调用 [super load];
会发现在该类 +load
时会发送 [super load]
消息, 所以会先调用父类的 +initialize
方法 (手动调用, 优先查找分类) , 然后才是父类的 +load
, 再然后才是当前类的 +load
方法.
如图 :
2019-03-28 +[NSThread(initialize) initialize] - DYLD 加载时调用,分类重载
2019-03-28 +[WXHuman load] - DYLD 加载时调用
2019-03-28 +[WXHuman(One) initialize] - 优先分类查找
2019-03-28 +[WXHuman(One) load] - 手动调用,优先分类查找
2019-03-28 +[WXPerson load] - [super load]
2019-03-28 +[WXHuman(One) load] - DYLD 加载时调用
2019-03-28 +[WXPerson(One) load] - DYLD 加载时调用
2019-03-28 +[NSThread(initialize) load] - DYLD 加载时调用
2019-03-28 +[WXPerson(Initialize) load] - DYLD 加载时调用
2019-03-28 main 方法入口打印
2019-03-28 +[WXPerson(Initialize) initialize] - DYLD 加载时调用
调用
新建 WXPerson : WXHuman
/ WXPerson(One)
/ WXPerson(Two)
/ WXHuman(One)
类, 这些类均重写了 +load
和 +initialize
方法:
2019-03-27 18:54:40.889322+0800 +[WXHuman load]
2019-03-27 18:54:40.889560+0800 +[WXPerson load]
2019-03-27 18:54:40.889575+0800 +[WXPerson(Initialize) load]
2019-03-27 18:54:40.889586+0800 +[WXPerson(One) load]
2019-03-27 18:54:40.889711+0800 +[WXHuman(One) initialize]
2019-03-27 18:54:40.889742+0800 +[WXPerson(One) initialize]
因为 +load
加载是遍历所有的类(只要是类)进行查找的, 所以只要实现就会调用;
而 +initialize
则会被分类重载方法所覆盖 (在 methodizeClass(cls) 方法实现时调用 attachList
方法时,分类添加是在类之后, 且添加 list 操作是向前添加, 所以在查找方法的时候会优先找到分类的方法), 所以只会调用分类的初始化方法, 当然可以调用 [super initialize]
调起父类 +initialize
方法.