initialize翻阅资料显示是 :会在类第一次收到消息的时候调用
1.我们知道 , 不管是调用类方法, 还是对象方法, 本质就是利用objc_msgSend消息机制给对象/类对象发送消息 ,通过isa指针去类对象 , 或者元类对象中寻找方法
2.回到initialize , 既然是第一次收到消息的时候会调用initialize ,最简单的办法就是去看底层实现 , 基于objc4-781
3.源码截图:
全是汇编啊 ,问题不大 ,对objc_msgSend调试
4.实验例子
教大家一些小技巧
1.Xcode -> Debug ->Debug Workflow->Always Show Disassembly 显示汇编
2.对[Person alloc] 下断点 , 然后控制台 b objc_msgSend 对objc_msgSend 函数下断点
3. 控制台 c :是跳转到下一个断点 控制台:ni:单步调 控制台:si:单步调,遇到方法调用就进入方法
4.汇编里面的函数跳转, 都是跟b有关的, cbz, bl, b.ne ,b , 看到这些就可以下个断点 ,无外乎就是有没有返回值, 是否比较之类的,但总是要跳转的
5.对objc_msgSend下断点
objc_msgSend的汇编实现 , 也可以对照objc4-781
中的objc_msgSend的汇编
libobjc.A.dylib`objc_msgSend:
-> 0x189b6c9a0 <+0>: cmp x0, #0x0 ; =0x0
0x189b6c9a4 <+4>: b.le 0x189b6ca20 ; <+128>
0x189b6c9a8 <+8>: ldr x13, [x0]
0x189b6c9ac <+12>: and x16, x13, #0xffffffff8
0x189b6c9b0 <+16>: ldr x11, [x16, #0x10]
0x189b6c9b4 <+20>: and x10, x11, #0xffffffffffff
0x189b6c9b8 <+24>: and x12, x1, x11, lsr #48
0x189b6c9bc <+28>: add x12, x10, x12, lsl #4
0x189b6c9c0 <+32>: ldp x17, x9, [x12]
0x189b6c9c4 <+36>: cmp x9, x1
0x189b6c9c8 <+40>: b.ne 0x189b6c9d8 ; <+56>
0x189b6c9cc <+44>: eor x12, x12, x1
0x189b6c9d0 <+48>: eor x12, x12, x16
0x189b6c9d4 <+52>: brab x17, x12
0x189b6c9d8 <+56>: cbz x9, 0x189b6cd20 ; _objc_msgSend_uncached
0x189b6c9dc <+60>: cmp x12, x10
0x189b6c9e0 <+64>: b.eq 0x189b6c9ec ; <+76>
0x189b6c9e4 <+68>: ldp x17, x9, [x12, #-0x10]!
0x189b6c9e8 <+72>: b 0x189b6c9c4 ; <+36>
0x189b6c9ec <+76>: add x12, x12, x11, lsr #44
0x189b6c9f0 <+80>: ldp x17, x9, [x12]
0x189b6c9f4 <+84>: cmp x9, x1
0x189b6c9f8 <+88>: b.ne 0x189b6ca08 ; <+104>
0x189b6c9fc <+92>: eor x12, x12, x1
0x189b6ca00 <+96>: eor x12, x12, x16
0x189b6ca04 <+100>: brab x17, x12
0x189b6ca08 <+104>: cbz x9, 0x189b6cd20 ; _objc_msgSend_uncached
0x189b6ca0c <+108>: cmp x12, x10
0x189b6ca10 <+112>: b.eq 0x189b6ca1c ; <+124>
0x189b6ca14 <+116>: ldp x17, x9, [x12, #-0x10]!
0x189b6ca18 <+120>: b 0x189b6c9f4 ; <+84>
0x189b6ca1c <+124>: b 0x189b6cd20 ; _objc_msgSend_uncached
0x189b6ca20 <+128>: b.eq 0x189b6ca58 ; <+184>
0x189b6ca24 <+132>: adrp x10, 318626
0x189b6ca28 <+136>: add x10, x10, #0x600 ; =0x600
0x189b6ca2c <+140>: lsr x11, x0, #60
0x189b6ca30 <+144>: ldr x16, [x10, x11, lsl #3]
0x189b6ca34 <+148>: adrp x10, 318626
0x189b6ca38 <+152>: add x10, x10, #0x568 ; =0x568
0x189b6ca3c <+156>: cmp x10, x16
0x189b6ca40 <+160>: b.ne 0x189b6c9b0 ; <+16>
0x189b6ca44 <+164>: adrp x10, 318626
0x189b6ca48 <+168>: add x10, x10, #0x680 ; =0x680
0x189b6ca4c <+172>: ubfx x11, x0, #52, #8
0x189b6ca50 <+176>: ldr x16, [x10, x11, lsl #3]
0x189b6ca54 <+180>: b 0x189b6c9b0 ; <+16>
0x189b6ca58 <+184>: mov x1, #0x0
0x189b6ca5c <+188>: movi d0, #0000000000000000
0x189b6ca60 <+192>: movi d1, #0000000000000000
0x189b6ca64 <+196>: movi d2, #0000000000000000
0x189b6ca68 <+200>: movi d3, #0000000000000000
0x189b6ca6c <+204>: ret
0x189b6ca70 <+208>: nop
0x189b6ca74 <+212>: nop
0x189b6ca78 <+216>: nop
0x189b6ca7c <+220>: nop
最笨的方法就是对每一个跟跳转地址有关的地方下断点,然后单步执行看最终去了哪里
小技巧是我发现这个objc_msgSend方法里面的跳转地址基本都是在函数内部跳转 ,从这个地址跳转到另一个地址, 所以我找了一个跳出当前方法的地址, 0x189b6cd20 ,我就对0x189b6cd20位置下了断点
命令行 按 C 跳转
libobjc.A.dylib`_objc_msgSend_uncached:
-> 0x189b6cd20 <+0>: pacibsp
0x189b6cd24 <+4>: stp x29, x30, [sp, #-0x10]!
0x189b6cd28 <+8>: mov x29, sp
0x189b6cd2c <+12>: sub sp, sp, #0xd0 ; =0xd0
0x189b6cd30 <+16>: stp q0, q1, [sp]
0x189b6cd34 <+20>: stp q2, q3, [sp, #0x20]
0x189b6cd38 <+24>: stp q4, q5, [sp, #0x40]
0x189b6cd3c <+28>: stp q6, q7, [sp, #0x60]
0x189b6cd40 <+32>: stp x0, x1, [sp, #0x80]
0x189b6cd44 <+36>: stp x2, x3, [sp, #0x90]
0x189b6cd48 <+40>: stp x4, x5, [sp, #0xa0]
0x189b6cd4c <+44>: stp x6, x7, [sp, #0xb0]
0x189b6cd50 <+48>: str x8, [sp, #0xc0]
0x189b6cd54 <+52>: mov x2, x16
0x189b6cd58 <+56>: mov x3, #0x3
0x189b6cd5c <+60>: bl 0x189b80368 ; lookUpImpOrForward
0x189b6cd60 <+64>: mov x17, x0
0x189b6cd64 <+68>: ldp q0, q1, [sp]
0x189b6cd68 <+72>: ldp q2, q3, [sp, #0x20]
0x189b6cd6c <+76>: ldp q4, q5, [sp, #0x40]
0x189b6cd70 <+80>: ldp q6, q7, [sp, #0x60]
0x189b6cd74 <+84>: ldp x0, x1, [sp, #0x80]
0x189b6cd78 <+88>: ldp x2, x3, [sp, #0x90]
0x189b6cd7c <+92>: ldp x4, x5, [sp, #0xa0]
0x189b6cd80 <+96>: ldp x6, x7, [sp, #0xb0]
0x189b6cd84 <+100>: ldr x8, [sp, #0xc0]
0x189b6cd88 <+104>: mov sp, x29
0x189b6cd8c <+108>: ldp x29, x30, [sp], #0x10
0x189b6cd90 <+112>: autibsp
0x189b6cd94 <+116>: braaz x17
0x189b6cd98 <+120>: nop
0x189b6cd9c <+124>: nop
这个_objc_msgSend_uncached 感觉内部非常清晰, 里面只有一个 bl 语句, lookUpImpOrForward
控制台 b lookUpImpOrForward
或者直接看runtime源码
大致看了下汇编, 发现了和源码中有同样的方法名,所以接下来同学是喜欢看源码, 还是继续看汇编都可以
和initialize相关的我们找下
// Check for +initialize 检查是否initialize
// behavior应该是是否要初始化,我觉得默认肯定是大于0的 , 是不是一会儿汇编调试下
// LOOKUP_INITIALIZE 全局搜索下 ,也是默认 = 1的
/*enum {
LOOKUP_INITIALIZE = 1,
LOOKUP_RESOLVER = 2,
LOOKUP_CACHE = 4,
LOOKUP_NIL = 8,
};*/
//!cls->isInitialized() :是否初始化 取反 , 也就是说, 没初始化的时候调用下面的函数
if ((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized()) {
initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
// If sel == initialize, initializeNonMetaClass will send +initialize
// and then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
initializeNonMetaClass 函数:
把一个类传进来 , 如果有父类 并且 父类的initialized没有初始化, 就再次调用initializeNonMetaClass 并把父类传进去
具体如何初始化的initialize, 接着往下看, 如果不清楚是哪个方法, 就都点进去看看, 看看哪个像, 通过实验以及资料
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
asm("");
}
让类对象去调用initialize方法 ,类对象通过isa指针 , 找到元类对象, 去元类对象的MethodList中去寻找initialize
六.验证一些东西
1.behavior 是否大于0
OC函数:
x1, 是调用者 ,
x2,是方法名
x3 ~x几(我忘记了)的是参数
C函数:
x1开始就是参数了,所以我打印了x1, 到x3, 因为lookUpImpOrForward 就3个参数
所以behavior是等于3
完整的调用顺序 , 也可以不看这个, 直接看最后的结论
1.当给类发消息的时候 ,objc_msgSend会判断是否初始化过initialized,如果没有,就去调用initializeNonMetaClass初始化initialized
2.当初始化initialized的时候, 会判断类的父类是否初始化过initialized , 如果没有, 则优先初始化父类的initialized
3.如果有Category的话,也会遵循, 先去Category的方法里寻找initialized(这个涉及到Category方法和类里面的方法执行顺序的问题)
3.父类的initialize只初始化一次 ,但是会被多次调用
4.子类的 isa去元类对象methodlist找不到,就会通过superClass去父类找, 就会找到父类的initialize调用
5.如果去父类找initialize, 就会去看父类的methodlist方法, 但是methodlist 是包含了父类的category方法的, 根据源码, methodlist数组的顺序是category的方法放前面, 父类本身的方法放后面, methodlist数组中category的编译顺序根据源码的话是 build phases ->compile sources中的编译顺序决定的, 后编译的放数组前面 , 所以如果父类有category, 并且category中也实现了initialize就回去找父类category的initialize,