一、alloc对象的指针地址和内存地址的分析
在进行alloc的源码分析,先通过例子查看指针地址和内存地址的区别:
打印分别输出内存、内存地址、指针地址,结果如下:
结果分析:
- alloc 开辟了内存空间,创建了对象,p1、p2、p3指向同一个内存空间。
LGPerson<0x280fd02c0> - init 的内存指针是一样的,为开辟内存空间,没有对指针进行操作,所以其内容和内存地址是相同的。
- init 所在栈中的指针地址不一样,是连续开辟指针地址;每个相隔8隔指针
NSLog的各种打印
%@ 对象
%p 指针地址
%p -> p1 对象的内存地址
%p -> &p1 对象的指针地址
二、alloc底层探索源码的方式
1. 符号断点方式直接跟流程 -> 底层源码: libobjc.A.dylib`objc_alloc:
-
在
LGPerson alloc处添加断点,运行工程,会停在LGPerson alloc的位置 -
按住
control键,点击Step into↓键 -
点击进去后,显示出
objc_alloc底层函数 -
继续查看底层源码结构,添加一个
objc_alloc符号断点- 下一步,显示了
objc_alloc的源码库的方法libobjc.A.dylib`objc_alloc:
- 下一步,显示了
2. 通过汇编查看跟踪流程
-
在
LGPerson alloc处添加断点,运行工程,会停在LGPerson all的位置 -
xcode 工具栏 选择
Debug --> Debug Workflow --> Always Show Disassembly,选中当前选择,进入汇编代码 -
按住
control键,点击Step into↓键,进入 symbol stub for: objc_alloc函数 -
按住
control键,点击Step into↓键,断点在objc_alloc函数 -
总结: ①xcode 工具栏 选择
Debug --> Debug Workflow --> Always Show Disassembly, ②添加一个objc_alloc符号断点,启动工程,执行到源码所在库libobjc.A.dylib`objc_alloc
3. 通过已知符号断点alloc 确定未知 : libobjc.A.dylib`+[NSObject alloc]:,结合第2步使用
- 在
LGPerson alloc处添加断点,运行工程,会停在LGPerson alloc的位置
-
添加alloc符号断点
-
直接通过已知符号断点
alloc确定未知:libobjc.A.dylib`+[NSObject alloc]:
三、源码分析alloc流程
- 准备工作
苹果开源源码汇总: opensource.apple.com
Source Browser: opensource.apple.com/tarballs/
objc4-818 源码:opensource.apple.com/tarballs/ob…
1. 流程分析
-
[第①步] 源码搜索
alloc {方法进入 alloc的方法 (源码分析开始) -
[第②步]跳转至
_objc_rootAlloc方法实现 -
[第③步]进入到
callAlloc方法如上图所示执行到第三步时,在callAlloc方法中,需验证执行的是
_objc_rootAllocWithZone还是objc_msgSend???设置符号断点调试,发现走的是
_objc_rootAllocWithZone流程补充说明
编译器优化
#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处设置断点,方式如下图: -
执行跟踪发现,调用
alloc函数,进入到callAlloc函数中,会调用一次objc_msgSend,发送一个alloc消息。执行流程解读:-
判断缓存中是否存在自定义的
alloc/allocWithZone地方实现,显然第一次运行类中是没有该方法缓存的。 -
cls是什么?类嘛?不是!看看源码的定义:
typedef struct objc_class *Class;。所以cls是一个指针,指向一个结构体,这个结构体也就是Core Foundation层的类! -
类的初始化在
read_images方法执行时,而实例对象的初始化在alloc的时候。 -
第一次执行
((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));,会进行慢速方法查找,找到NSObject类的alloc方法,并将方法放入方法缓存。 -
所以除了第一调用
alloc方法外,之后在进行对象初始化会直接走_objc_rootAllocWithZone方法。
-
四、Alloc 核心方法
_class_createInstanceFromZone 分析
1. cls->instanceSize 计算内存大小
执行了编译器优化,执行缓存中cache.fastInstanceSize方法,计算所需要的内存空间大小
2. calloc
向系统申请开辟内存,返回地址指针。此流程会临时分配一个内存,调用calloc后分配的内存空间才是创建对象的内存地址。
在平常的开发中,一般一个对象的打印的格式都是类似于这样的<LGPerson: 0x280fd02c0>(是一个指针)。为什么这里不是呢?
- 主要是因为
objc 地址还没有与传入 的cls进行关联, - 同时印证了
alloc的根本作用就是开辟内存
3. obj->initInstanceIsa:与isa关联
关联到相应的类,即将开辟的内存空间指向所要关联的类! 通过运行结果发现,在调用obj->initInstanceIsa之前,obj只有一个内存地址,而调用之后明确了对象类型为LGPerson。
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字节对齐,是由于在一个对象中,第一个属性
isa占8字节,当然一个对象肯定还有其他属性,当无属性时,会预留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对象初始化分析流程图 :