在iOS开发的过程中,我们最熟悉的就是对象,经常会使用到的一个函数:
alloc
,那这个函数的底层到底做了什么呢 ?我们一起一探究竟。
开始探索前,先看一下探索过程中可能用到的一些指令!
一、常用指令
1. po: 为 print object 的缩写,显示对象的文本描述
2. bt: 打印函数的堆栈
3. register read 读取寄存器
4. x/nuf
n表示要显示的内存单元的个数
u表示一个地址单元的长度:
取值范围:
b 单字节
h 表示双字节
w 表示四字节
g 表示八字节
f表示显示方式:
取值范围:
x 按十六进制格式
d 按十进制格式
u 按十进制格式显示无符号
o 按八进制格式
t 按二进制格式
a 按十六进制格式
i 指令地址格式
c 按字符格式
f 按浮点数格式
持续更新中...
二、alloc做了什么?
通过以下代码我们可以知道alloc是向系统申请内存空间
JLPerson *p1 = [JLPerson alloc];
JLPerson *p2 = [p1 init];
JLPerson *p3 = [p1 init];
NSLog(@"%@-%p-%p",p1,p1,&p1);
NSLog(@"%@-%p-%p",p2,p2,&p2);
NSLog(@"%@-%p-%p",p3,p3,&p3);
-----------------------------------------------------------
<JLPerson: 0x6000032c8020>-0x6000032c8020-0x7ffeef26e1a8
<JLPerson: 0x6000032c8020>-0x6000032c8020-0x7ffeef26e1a0
<JLPerson: 0x6000032c8020>-0x6000032c8020-0x7ffeef26e198
从上面的代码中可以看出p1、p2、p3的指针地址之间是相差8个字节,并且地址是连续
的,这就符合栈内存的分配原则
。
根据代码的演示我们可以得到以下的图示。
总结:指针地址是在
栈内存
,申请的内存空间在堆内存
。
三、alloc底层是怎么调用?
我们已经知道了alloc
是申请内存空间,那么它是怎么申请内存的呢?申请多少内存空间?内存的大小怎么计算?
带着这些问题往下探索。
三种探索底层的方式:
- 下符号断点的形式直接跟流程: Symbolic Breakpoint
- 按住control -> step into
- 汇编查看跟流程:Debug -> Debug workflow -> Always show Disassembly
通过上面三种方式我们知道了alloc
底层是属于libobjc
库,我们将源码下载编译跑起来。
苹果开源源码汇总: https://opensource.apple.com
这个地址⽤的更直接: https://opensource.apple.com/tarballs/
-
发现问题
首先我们对下载的源码进行编译,对alloc函数
进行断点跟踪(也可以使用符号断点或者汇编的方式进行),按照正常的思维流程应该是响应alloc函数的底层调用
,但是真正的调试却是走了objc_alloc
,这是为什么呢?
-
探索问题
-
通过对源码进行全局搜索
objc_alloc
,对结果一个个解读我们可以从中发现一个函数fixupMessageRef
,里面有一个if (msg->sel == @selector(alloc))
判断,满足条件就是msg
指向的imp
替换成objc_alloc
。
既然找到了
fixupMessageRef
,那么我顺着这条思路找一找fixupMessageRef
是什么时候调用的呢?
通过逆向的查找我们可以得出以下的一个调用流程:
fixupMessageRef
<--_read_images
<--map_images_nolock
<--map_images
<--_dyld_objc_notify_register
<--_objc_init
把这些函数全部打上断点,运行程序,看是否如我们所想的那样进行了IMP的替换
;运行后发现还是会走objc_alloc方法,但是并没有走fixupMessageRef
方法进行替换。为什么会提供一个不被执行的修复函数呢?难道是因为在编译的过程中就有可能发生问题,然后做一个容错的处理吗?找到
LLVM
的源码,通过解读LLVM的源码可以得出alloc、release、autoRelease
等一些方法在编译的过程中LLVM
会对这些函数进行Hook
拦截
我们已经知道了为什么要走objc_alloc
方法了,那对于alloc
主线的流程通过断点方式跟下来就可以了。
-
alloc调用流程图:
- alloc的主线流程图我们已经比较清晰了,接下来我们重点看一下
_class_createInstanceFromZone
这个函数的实现。
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
...
size_t size;
# 计算当前类需要开辟的内存空间大小
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
# 申请内存空间
obj = (id)calloc(1, size);
}
...
if (!zone && fast) {
# 将类cls和obj指针进行关联
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
- 我们重点看一下
instanceSize
这个函数,主要用来计算当前类需要开辟的内存空间大小。
看一下函数的整个流程图:
字节对齐算法:
问题1:为什么alloc第一次会进objc_alloc,然后才会进去_objc_rootAlloc ?
(LLVM底层对objc_alloc进行拦截)
扩展:
- init 初始化,使用
工厂模式
,可以对其进行重写
,用来扩展 - new 底层是
alloc
和init
的组合,直接使用new
相对于使用alloc init
扩展性更差了
四、内存对齐原则
前言:
1.属性和成员变量会影响内存大小,方法不影响内存大小
2.oc对象开辟内存空间大小是以16字节对齐
,对象的成员变量的字节是以8字节对齐
各类型字节大小:
问题1:为什么需要字节对齐?
- 通常内存是由字节组成,cpu在存取数据时,是以
块
为单位存取,块
的大小决定了内存存取的力度。频繁的存取未对齐的数据,会降低cpu的性能,所以可以通过内存对齐
的方式来减少存取次数
,从而达到降低cpu的开销
,以空间
来换取时间
。
问题2:为什么oc对象开辟内存空间是以16字节对齐?
- 由于在一个对象中,第一个属性
isa
占8字节
,一个对象中肯定还会包含其他的属性
和成员变量
,系统会预留8字节
,即16字节对齐
,而如果是8字节对齐
的话,该对象的isa
和下一个对象的isa
紧挨着,访问时容易造成访问混乱。 - 16字节对齐,可以加快
cpu读取速度
,也可以使访问更加安全
。
下面我们看一下结构体的内存对齐
struct LGStruct1 {
double a; // 8 [0 7]
char b; // 1 [8]
int c; // 4 [12 13 14 15] (9 10 11空3个字节 12是4的倍数)
short d; // 2 [16 17]
}struct1;
# 根据字节对齐是8字节原则 8的倍数最后为 24
struct LGStruct2 {
double a; // 8 [0 7]
int b; // 4 [8 9 10 11] (8是4的倍数)
char c; // 1 [12]
short d; // 2 [14 15] (13 空1个字节 14是2的倍数)
}struct2;
# 根据字节对齐是8字节原则 8的倍数最后为 16
从上述代码中可以看出,结构体的属性都一样,属性的顺序不一样,内存大小也不一样。
上面是单个结构体的内存对齐,如果结构体嵌套又是怎样的呢?
struct LGStruct1 {
double a; // 8
char b; // 1
int c; //4
short d; // 2
}struct1;
struct LGStruct3 {
double a; //8 [0 --> 7]
int b; //4 [8 9 10 11 ]
char c; //1 [12]
short d; //2 [14 15]
int e; //4 [16 17 18 19]
struct LGStruct1 {
double a; // 8 [24 --> 31]
char b; // 1 [32]
int c; //4 [36 37 38 39]
short d; // 2 [40 41]
}str;
}struct3;
# 将struct3进行展开, 根据8字节内存对齐原则,最终输出为 48
总结:
一般结构体大小
- 1.结构体成员的偏移量必须是成员大小的整数倍
- 2.结构体大小是最大元素的倍数(最大元素字节对齐)
嵌套结构体大小
- 1.展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员变量的整数倍
- 2.结构体大小必须是所有成员中最大元素的整数倍(8字节对齐)
如果以上内容有错误的地方,还请各位大佬指点!
持续更新和修复中...