OC对象的初始化流程

ow.png

OC对象的内存和指针

  • 我们每天都在写[[xxx alloc] init],但从未探究过allocinit内部都干了什么?带着疑问,我们通过一段简单代码,先来看看OC对象的初始化:
    FFObj *obj = [FFObj alloc];
    FFObj *o1  = [obj init];
    FFObj *o2  = [obj init];
    NSLog(@"%@ -- %p -- %p", obj, obj, &obj);
    NSLog(@"%@ -- %p -- %p", o1, o1, &o1);
    NSLog(@"%@ -- %p -- %p", o2, o2, &o2);
复制代码
  • 通过上面的代码运行打印出的结果是:
    <FFObj: 0x600003a28170> -- 0x600003a28170 -- 0x7ffee41dcc38
    <FFObj: 0x600003a28170> -- 0x600003a28170 -- 0x7ffee41dcc30
    <FFObj: 0x600003a28170> -- 0x600003a28170 -- 0x7ffee41dcc28
复制代码
  • 图示:

alloc.png

  • 结论:
    • 通过三个对象的内存地址可以看出,alloc方法开辟了内存,而init方法没有对内存做操作,这三个对象是一模一样的.
    • 通过三个指针地址可以看出,三个指针都指向了堆空间(0x6开头是堆)内的同一块区域,而三个指针自身则是在栈(0x7开头是栈)中开辟的三个连续空间.

那么,alloc是如何开辟内存空间的呢?init又是否真的什么都没做呢?我们继续往下看.

寻找alloc

查找alloc的位置以及底层源码调用流程,可以通过:

  • 断点跟踪
    • 先将alloc的调用位置添加断点

    • breakpoint.jpg

    • 按住control键点击下一步,跟踪查看:

    • objc_alloc.png

    • 查找到关键函数objc_alloc

  • 汇编分析
    • 在断点位置,通过点击Xcode导航栏的Debug->Debug Workflow->Always Show Disassembly,查看汇编代码:
    • WX20210701-163246@2x.png
    • 查找到关键函数objc_alloc
  • 通过已知函数符号断点,比如直接将alloc添加进符号断点,然后跟踪查看.
  • 得到关键函数objc_alloc后,我们将它也加入到符号断点中:
    • 000.png

最终得知,objc_alloclibobjc.A.dylib中,而这正是objc框架的底层源码所在,好在这部分代码苹果是开源了的,我们可以下来源码,来继续探查.

alloc源码流程分析

  • 通过断点调试或者全局搜索找到alloc方法
    • alloc.png
  • _objc_rootAlloc

    • _objc_rootAlloc.png
  • callAlloc

    • callAlloc.png
  • 到了callAlloc之后,不再是简单的方法逐层封装调用了,而是出现了大量的逻辑,也就是说,我们要从这里开始分析源码逻辑了.
  • 补充说明
    • #if __OBJC2__表示该代码块内的代码属于objc2.0版本,也正是我们当前使用的版本.
    • fastpath的宏定义为#define fastpath(x) (__builtin_expect(bool(x), 1)),表示当前的if判断,有更高的几率为true
    • slowpath的宏定义为#define slowpath(x) (__builtin_expect(bool(x), 0)),表示当前的if判断,有更高的几率为false
    • __builtin_expect,用法为__builtin_expect(bool(x), y),表示布尔值bool(x)有更高的几率为y,能够让编译器在编译阶段将此处的代码跳转进行逻辑优化,得到更高性能的汇编代码.
  • _objc_rootAllocWithZone

    • _objc_rootAllocWithZone.png
  • _class_createInstanceFromZone

    • 看来我们终于走到了alloc流程中最核心的部分,该方法内部进行了内存的计算和分配逻辑
    • _class_createInstanceFromZone.png
    • instanceSize计算所需内存空间的大小
    • 根据if (zone)判断来调用malloc_zone_calloc或者calloc进行内存分配
    • calloc之前,分配给obj的是一块脏内存,执行calloc之后,obj才真的分配到了内存,calloc执行前后的obj内存见下图
    • callloc.png
    • lldb.png
    • 此时po出来的obj是只有内存地址而没有类型的,说明此时的obj没有绑定到类
    • 通过if (!zone && fast)判断分别调用obj->initInstanceIsa(cls, hasCxxDtor)obj->initIsa(cls)来初始化isa,将obj绑定到类
    • 通过if (fastpath(!hasCxxCtor))判断,直接返回obj或者返回object_cxxConstructFromClass(obj, cls, construct_flags)
  • instanceSize

    • instanceSize.png
    • 当有缓存的时候,会走到fastInstanceSize
    • 没有缓存的时候,会走到alignedInstanceSize
    • 如果最终得到的size < 16,则会返回16
  • alignedInstanceSize

    • alignedInstanceSize.png
    • word_align.png
    • word_align 字节对齐算法中参数x的值取自unalignedInstanceSize(),即data()->ro()->instanceSize,实例变量的大小,由ivars决定.
    • WORD_MASK的值在64位中为7,在32位中为3
    • 字节对齐算法说明(64位)

      • (x + WORD_MASK) & ~WORD_MASK
      • x = 8 WORD_MASK = 7
      • (8 + 7) & ~7 = 15 & ~7
      • 0000 1111 & ~0000 0111 = 0000 1111 & 1111 1000
      • 结果为0000 1000 = 8
      • (x + y) & ~y计算的是y + 1的整数倍数, 等同于 (x + y) >>z <<z,其中z是以2为底y的对数,即y = 8 时 z = 3
      • 由此可知,alignedInstanceSize()最终的计算结果是以8字节对齐,取8的倍数
    • 那么,为什么要以8字节对齐开辟内存呢?为什么最终分配内存如果<16=16呢?
      • 单位长度最高为8,比如指针,其他常用数据类型都可以被8以内的大小存储下来.
      • 恒定以8为单位存储数据后,CPU也可以恒定以8为单位去读取数据,不需要不停地变更存取长度,这是通过以空间换时间的方式,提高CPU存取效率.
      • 16为最小开辟空间是因为类会至少有一个isa成员,而isa结构体指针类型,长度为8,开辟出更多的空间时为了容错处理.
  • fastInstanceSize

    • fastInstanceSize.png
    • 调用align16实现16字节对齐
    • align16.png
  • initInstanceIsa

    • initInstanceIsa.png
    • initInstanceIsa会调用initIsa
  • initIsa

    • initIsa1.png
    • initIsa2.png
    • initIsa会对isa进行绑定
    • if (!nonpointer)判断表示是否进行指针优化

补充:alloc流程图

alloc流程图.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享