Objective-C的+initialize方法调用原理分析

961 阅读6分钟

Objective-C的+load方法调用原理分析

Objective-C之Category的底层实现原理

Objective-C为我们提供了两种方法去运行对类进行相关设置的代码。

  • +load:该方法会在很早阶段(同时也是比较危险的阶段,可能导致崩溃)被调用,一旦某个类被Runtime加载,该类的+load方法就会被调用。我们可以在这个方法里面写一些必须要在程序运行非常早期阶段就需要运行的代码。
  • +initialize:该方法可以比较安全的处理大部分情况下的设置任务代码,因为会在一个更加安全的环境下被调用。你几乎可以在这个方法里面做任何事情,除非,你的代码需要等到外部实体向这个类发消息之后,才能运行,那么将你的代码放在+initialize方法里面将是不合适的。

关于+initialize方法的一些结论

  • +initialize方法会在类第一次接收到消息的时候调用
  • +initialize方法是通过objc_msgSend()进行调用的

分析

+initialize方法的调用可能的发生的地方

+initialize既然是在类对象第一次接受消息的时候调用,我们知道接受消息整个逻辑的底层其实就是通过objc_msgSend(Class cls, SEL sel)函数开始的,而该函数主要思路就是首先通过isa先进行方法的查找,找到后就进行方法调用。所以系统对+initialize的调用,就可能发生在上述的两个步骤之中。

撸一波源码

那么我们来看看这个函数的源码,通过关键字objc_msgSend(来搜索一下,结果如下图objc_msgSend的汇编实现

可以看到能在源码里面找到的相关文件都是一堆.s文件,也就是汇编文件,说明源码提供了该函数的汇编实现,这是一种半开源形式,想要读懂,就需要汇编基础。令人悲伤的是,此时此刻码字的我,还不会汇编,自然也就看不懂。 image.png随便点开一个,比如说arm64.s的文件,看到如此晦涩的汇编代码,我真的有心无力,只能默默鼓励自己:学海无涯,前路漫漫~~ 不过发现注释里面有一句代码,是跟方法查询相关的函数,objc_msgLookup(id self, SEL _cmd, ...),那么继续查看一下,万一是看得懂的C函数呢,走起 结果还是令人失望,除了一个系统的函数定义,没有找到相关的实现。 看来这个方向是暂时走不通了。还好,我从大佬MJ老师那里,了解到一个跟objc_msgLookup等价的方法,它就是Method class_getInstanceMethod(Class cls, SEL sel)。进入该方法查看一下 我们在objc-runtime-new.mm文件下(很明显objc-runtime-old.mm应该是过时的源码)看到该函数的实现里面,有一个lookUpImpOrNil函数,这个便是具体的方法查找函数,继续进入其中 里面又包了一层,话不多说,继续进入函数lookUpImpOrForward 终于我们发现了想要的东西,该函数里面,可以开到在一开始,就有一段与类对象初始化相关的逻辑,如上图红框,我把它转成伪代码的形式便于理解

if (需要初始化  &&  class还没进行过初始化) {
        对class进行初始化
    }

️️️注意,上面这段为代码逻辑,是发生在方法查找过程的,也就是说,类对象每次接收到消息,进行方法查找的时候,都会进入这段逻辑,很明显,该逻辑中,if判断条件就确保了,对于类对象的初始化操作只会进行一次,并且发生在类对象第一次接收到消息的时候。

那么看看对类对象进行初始化的具体过程,也就是_class_initialize函数,进入 针对我们研究的问题,我们找到关键部分代码,该函数里面,先判断了父类是否被initialized,如果没有的话,递归调用本函数对父类进行处理,完毕之后,在通过callInitialize()+initialize进行实际调用。

callInitialize()的实现,也证实了,系统确实是通过消息机制objc_msgSend()来调用+initialize方法的。好了,源代码分析结束。

上机调试

场景一 CLPerson的+initialize方法

小结(一):该场景证明了,+initialize方法的调用发生在类对象第一次接受消息的时候。

场景二 分类的编译顺序 CLPerson+Cate01的+initialize方法 CLPerson+Cate02的+initialize方法

小结(二):该场景证明了,系统对+initialize方法的调用是通过消息机制,也就是objc_msgSend函数来发起的,根据我的Objective-C之Category的底层实现原理一文对Category的“方法覆盖”现象的研究,也是支持该场景下的最后日志打印结果:打印的是--CLPerson+Cate02+initialize方法--。

场景三 CLStudent继承自CLPerson CLStudent的+initialize方法 CLTeacher继承自CLPerson CLTeacher的+initialize方法

小结(三):该场景证明了我们从源码中发现的逻辑:在+initialize方法都实现了的前提下,系统对一个类对象调用+initialize方法的之前,会先调用其父类的+initialize方法(️要求父类的+initialize方法必须从来没有被调用过)

场景四 接着上面的场景三,我们对CLStudentCLTeacher的进行微调,不实现他们的+initialize 注销CLStudent的+initialize实现 注销CLTeacher的+initialize实现

小结(四):从结果看,CLPerson+initialize方法被调用了三次。

  • 第(1)次调用,是CLStudent首次接受消息时,系统对父类CLPerson进行的+initialize调用,也就是objc_msgSend([CLPerson class] ,@selector(initialize))
  • 第(2)次调用,是CLStudent首次接受消息时,系统对CLStudent进行的+initialize调用,也就是objc_msgSend([CLStudent class],@selector(initialize)),因为CLStudent没有实现自己+initialize方法,所以根据消息机制的原理,调用了父类CLPerson+inilialize方法。
  • 第(3)次调用,是CLTeacher首次接受消息时,系统对CLTeacher进行的+initialize调用,也就是objc_msgSend([CLTeacher class],@selector(initialize)),因为CLTeacher没有实现自己+initialize方法,所以根据消息机制的原理,调用了父类CLPerson+inilialize方法。

总结

  • +initialize方法会在类对象 第一次 接收到消息的时候调用
  • 调用顺序:调用某个类的+initialize之前,会先调用其父类的+initialize(前提是父类的+initialize从来没有被调用过)
  • 由于+initialize的调用,是通过消息机制,也就是objc_msgSend(),因此如果子类的+initialize没有实现,就会去调用父类的+initialize
  • 基于同样的原因,如果分类实现的+initialize,那么就会“覆盖”类对象本身的+initialize方法而被调用。

好了,关于initialize的调用原理分析,就到这里结束了,各位看官有空常来

Objective-C的+load方法调用原理分析

Objective-C之Category的底层实现原理

特别备注

本系列文章总结自MJ老师在腾讯课堂开设的OC底层原理课程。