iOS底层原理 01:OC对象初始化之alloc流程分析

466 阅读5分钟

一、alloc对象的指针地址和内存地址的分析

在进行alloc的源码分析,先通过例子查看指针地址和内存地址的区别:

image.png

打印分别输出内存、内存地址、指针地址,结果如下:

image.png

结果分析:

  1. alloc 开辟了内存空间,创建了对象,p1、p2、p3指向同一个内存空间。 LGPerson<0x280fd02c0>
  2. init 的内存指针是一样的,为开辟内存空间,没有对指针进行操作,所以其内容和内存地址是相同的。
  3. init 所在栈中的指针地址不一样,是连续开辟指针地址;每个相隔8隔指针

截屏2021-08-28 上午10.17.56.png

NSLog的各种打印

%@ 对象
%p 指针地址
%p -> p1 对象的内存地址
%p -> &p1 对象的指针地址

二、alloc底层探索源码的方式

1. 符号断点方式直接跟流程 -> 底层源码: libobjc.A.dylib`objc_alloc:
  • LGPerson alloc处添加断点,运行工程,会停在LGPerson alloc的位置

    image.png

  • 按住control键,点击 Step into

    2021-06-14_212145.png

  • 点击进去后,显示出objc_alloc底层函数

    image.png

  • 继续查看底层源码结构,添加一个objc_alloc符号断点

    image.png

    • 下一步,显示了objc_alloc的源码库的方法libobjc.A.dylib`objc_alloc:

    image.png

2. 通过汇编查看跟踪流程
  • LGPerson alloc处添加断点,运行工程,会停在LGPerson all的位置

    image.png

  • xcode 工具栏 选择 Debug --> Debug Workflow --> Always Show Disassembly,选中当前选择,进入汇编代码

    image.png

  • 按住control键,点击 Step into 键,进入 symbol stub for: objc_alloc函数

    image.png

  • 按住control键,点击 Step into 键,断点在objc_alloc函数 image.png

  • 总结: ①xcode 工具栏 选择 Debug --> Debug Workflow --> Always Show Disassembly, ②添加一个objc_alloc符号断点,启动工程,执行到源码所在库libobjc.A.dylib`objc_alloc

    image.png image.png

3. 通过已知符号断点alloc 确定未知 : libobjc.A.dylib`+[NSObject alloc]:,结合第2步使用
  • LGPerson alloc处添加断点,运行工程,会停在LGPerson alloc的位置

image.png

  • 添加alloc符号断点

    截屏2021-08-28 上午11.12.05.png

  • 直接通过已知符号断点 alloc确定未知: libobjc.A.dylib`+[NSObject alloc]:

    截屏2021-08-28 上午11.15.21.png

三、源码分析alloc流程

  • 准备工作

苹果开源源码汇总: opensource.apple.com
Source Browser: opensource.apple.com/tarballs/
objc4-818 源码:opensource.apple.com/tarballs/ob…

1. 流程分析
  • [第①步] 源码搜索alloc {方法进入 alloc的方法 (源码分析开始)

    截屏2021-08-28 下午3.10.48.png

  • [第②步]跳转至 _objc_rootAlloc方法实现

    截屏2021-08-28 下午3.12.50.png

  • [第③步]进入到 callAlloc方法

    截屏2021-08-28 下午3.15.43.png

    如上图所示执行到第三步时,在callAlloc方法中,需验证执行的是 _objc_rootAllocWithZone 还是 objc_msgSend ???

    设置符号断点调试,发现走的是_objc_rootAllocWithZone流程

    截屏2021-08-28 下午3.35.45.png

    补充说明

    编译器优化
    #define fastpath(x) (__builtin_expect(bool(x), 1))
    #define slowpath(x) (__builtin_expect(bool(x), 0))

    fastpath:主要针对发布版本,对执行的流程进行深度优化,提高运行速度 slowpath: 执行流程都要执行,无优化执行流程

2. alloc的主线流程

alloc流程跟踪上部分已完成,但是alloc到底做了什么还未完成???

  • 打开源码代码,设置断点在main函数中设置断点在 LGPerson *p = [LGPerson alloc] ;上, 程序执行到 LGPerson *p = [LGPerson alloc];分别在alloc _objc_rootAlloc处设置断点,方式如下图:

    截屏2021-08-28 下午4.32.10.png

    截屏2021-08-28 下午4.32.23.png

    截屏2021-08-28 下午4.32.36.png

  • 执行跟踪发现,调用alloc函数,进入到callAlloc函数中,会调用一次objc_msgSend,发送一个alloc消息。

    截屏2021-08-28 下午4.44.06.png

    执行流程解读:

    1. 判断缓存中是否存在自定义的alloc/allocWithZone地方实现,显然第一次运行类中是没有该方法缓存的。

    2. cls是什么?类嘛?不是!看看源码的定义:typedef struct objc_class *Class;。所以cls是一个指针,指向一个结构体,这个结构体也就是Core Foundation层的类

    3. 类的初始化在read_images方法执行时,而实例对象的初始化在alloc的时候。

    4. 第一次执行((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));,会进行慢速方法查找,找到NSObject类的alloc方法,并将方法放入方法缓存

    5. 所以除了第一调用alloc方法外,之后在进行对象初始化会直接走_objc_rootAllocWithZone方法

四、Alloc 核心方法

_class_createInstanceFromZone 分析

1. cls->instanceSize 计算内存大小

截屏2021-08-28 下午6.35.43.png

执行了编译器优化,执行缓存中cache.fastInstanceSize方法,计算所需要的内存空间大小

2. calloc

向系统申请开辟内存,返回地址指针。此流程会临时分配一个内存,调用calloc后分配的内存空间才是创建对象的内存地址。

截屏2021-08-28 下午5.11.03.png

在平常的开发中,一般一个对象的打印的格式都是类似于这样的<LGPerson: 0x280fd02c0>(是一个指针)。为什么这里不是呢?

  • 主要是因为objc 地址 还没有与传入 的 cls进行关联,
  • 同时印证了 alloc的根本作用就是 开辟内存
3. obj->initInstanceIsa:与isa关联

关联到相应的类,即将开辟的内存空间指向所要关联的类! 通过运行结果发现,在调用obj->initInstanceIsa之前,obj只有一个内存地址,而调用之后明确了对象类型为LGPerson

截屏2021-08-28 下午5.10.22.png

4. init
```
- (id)init {
    return _objc_rootInit(self);
}
```
进入`_objc_rootInit`方法

```
id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}
```
`init`方法返回的是对象
`init`初始化构造,方便重写。便于扩展
5. new
```
+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
```
源码说明,`new`执行了`callAlloc` -> `init`, `new`=>`alloc` + `init`




补充说明

内存优化

定义的类没有属性,只继承NSObject,则这个实例实际占用的大小为8字节,

static inline uint32_t word_align(uint32_t x) {

    return (x + WORD_MASK) & ~WORD_MASK;

}

1、8 - > NSObject

2、最少16

  • 通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数降低cpu的开销

  • 16字节对齐,是由于在一个对象中,第一个属性isa8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱

  • 16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况

3、字节对齐:(x + WORD_MASK) & ~WORD_MASK;
(8 + 7) & ~7  ——> 15 & ~7   8字节对齐 取8的整数
15: 0000 1111
!7:   1111 1000
7 :   0000 0111
15 & ~7: 0000 1000 = 8

为什么8为倍数  以空间换取时间,整个内存8字节最多的   

OC对象初始化分析流程图 :

截屏2021-08-30 上午5.41.26.png