一、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对象初始化分析流程图 :