+load VS +initialize

调用时机


+load

通过上一篇 从 MachO 加载到对象创建! 可以了解到:

  • DYLD 在初始化主程序时发起 load_images 回调, 从而调用所有类以及分类的 +load 方法 (当然, 还有属性 / 协议, 依次是 方法-属性-协议-分类).

+initialize

  • 新建类 WXHuman 并重写 +initialize 方法;
  • 因为 +initialize 是类方法, 调用的时候是通过 objc_msgSend 调用, 所以在新建类且没有对该类作任何方法调用的时候, 此时 +initialize 不会调用 !
  • 在调用 [WXHuman alloc] 时, runtime 通过 objc_msgSend 给 Person 类对象发送 alloc 消息, 当断点调试 +initialize 方法时,可以看到调用栈:
initialize 调用栈
  • 图注 : 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 方法.

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

推荐阅读更多精彩内容