大拿之路,任重道远,永不言弃!!!
上一篇文章对象初始化探索中,学习了OC对象的初始化流程,以及对象内存按照16字节对齐
方式进行开辟过程。但是还留下了一些问题没有搞明白,比如:
LGPerson * person = [[LGPerson alloc] init];
,-init()方法
的作用是什么呢?LGPerson * newlg = [LGPerson new];
,+new()方法
的作用是什么呢?- 对象初始化过程中调用
alloc
为什么会走到objc_alloc
呢? - 系统会按照
16字节对齐
方式开辟内存,那么影响内存大小的因素是什么呢?
一.OC对象初始化补充
先解决上一篇对象初始化中留下的问题:-init()
,+new()
作用是什么?直接看源码吗?肯定不合适。要想分析他们的功能,先要知道他们运行流程是什么,我们采用汇编+下符号断点
的方式进行探索!
1.-init()分析
在LGPerson * person = [[LGPerson alloc] init];
这行代码中添加断点,并设置:Debug -> Debug Workflow -> Always Show Disassembly
。运行程序,结果如下图:
发现该过程实际调用了objc_alloc_init方法
,添加符号断点objc_alloc_init
,查看运行情况,见下图:
objc_alloc_init
方法依然来自libobjc.A.dylib库
,同时[[LGPerson alloc] init];
过程是调用了callAlloc方法创建对象
之后,向该对象发送init消息
。这里再添加init符号断点
,继续运行程序:
因为LGPerson
没有实现-init()方法
,所以通过方法查找
,最终会找到NSObject的init方法
,运行-[NSObject init];
流程,最终会调用到_objc_rootInit方法
。
结合源码,设置断点进行调试跟踪,流程和我们在汇编+符号断点的流程是一致的,如下图所示!最终返回的内容是callAlloc
创建的对象自身。
总结流程
:说明init方法
只是一个构造方法,并不涉及到对象的初始化创建。
2.+new()分析
探索方式和-init()
一样,这里不再进行汇编+下符号断点的流程演示,直接上源码。LGPerson * newlg = [LGPerson new];
并不会直接调用+new()
,而是执行了objc_opt_new方法
,见下图:
这里有类似callAlloc
的判断流程,如果初次初始化,hasCustomCore()会返回true
,见下图,这样会进入发送消息流程,即发送new消息
。
因为LGPerson
没有实现new类方法
,所以在进行方法查找过程中会找到NSObject
中,最终执行+[NSObject new];
方法。见下图所示:
继续执行程序,会走到_objc_rootAllocWithZone
中进行对象初始化。
总结流程:
+new()方法
是一个类方法,内部是对[[cls alloc] init];
流程的封装。
虽然+new()
是对[[cls alloc] init];
的封装,但是依然建议使用[[cls alloc] init];
进行对象的初始化!因为init作为构造器
,可以自定义提供自己所需要的初始化方法。
二.LLVM优化alloc
**
理解的还不够透彻,写下来也是疑点重重!
待补充……
**
三.对象内存的影响因素
上一篇文章我们已经知道,系统会以16字节对齐的形式开辟对象内存空间
的,同时对象的实际占用内存的大小采用的是8字节对齐算法
。
1.内存对齐说明
内存对齐可以理解为结构体中的成员申请内存大小时,系统最小分配的内存为8字节,是按每次8字节来申请的,不够8字节的也会申请8字节,然后按结构体中成员变量顺序再次申请,直到所有成员变量都能放下为止。
当结构体成员内存小的在前面时,会因为内存对齐的原因比较浪费内存,为了解决这个问题苹果中采用空间换时间,将类中的属性进行重排,以此来达到内存优化的目的。
2.内存影响因素案例说明
那么对象内存的影响因素有哪些呢?结合案例进行分析!这里有一个GFPerson类
,类中有两个属性(NSString *) - lgName、nikeName
,通过class_getInstanceSize(GFPerson.class)
获取创建的对象【至少】
需要的内存大小。如下图所示:
- 为什么是24呢?因为
GFPerson继承NSObject,还有一个8字节的isa也需要占用空间
。所以至少需要的内存空间是8 + 8 + 8 = 24,正好是8字节对齐
。 - 调整一下再添加一个
int - 属性age
,此时内容共占用为32!因为8 + 8 + 8 + 4 = 28,8字节对齐算法,所以占用内存为32
。 - 再次调整,
添加两个方法,一个对象方法,一个类方法
,运行发现此时已然是32。说明方法不会影响内存的占用
。 - 添加一个
成员变量double - height;
,运行结果为40,8 + 8 + 8 + 4 + 8 = 36,8字节对齐,所以输出结果为40
。
总结一下:
从以上的案例中可以得出,影响对象大小的因素是属性和成员变量,方法不会影响!同时只要有这个属性就会占用这份空间,即使不初始化也会占用!
3.内存结构解析
结合上面的例子,GFPerson
提供以下成员变量和属性:height(double)、lgName(NSString *)、nickName(NSString *)、age(int)、score(double)、ch1(char)、ch2(char)、ch3(char)
。其中height未被初始化
,运行程序,打印内存结构,以及对象占用空间大小。见下图所示:
总结说明:
- 因为对象占用
48个字节
,所以前6个8字节的数据时gf1的存储内容,从0x100b450e0
地址开始就不是gf1的数据内容了! age、ch1、cha2、ch3
系统进行了内存优化
,共占8个字节
。浮点数
输出是需要特殊处理一下采用p/f
,或者e -f f --
。
四.结构体内存对齐
在对象内存分析的基础上做个引申,我们来探讨一下结构体的内存对齐方式。
1.结构体内存对齐的原则
- 数据成员对⻬规则:
结构(struct)
(或联合(union)
)的数据成员,第⼀个数据成员放在offset为0
的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始
(⽐如int为4字节,则要从4的整数倍地址开始存储。min(当前开始的位置m n) m = 9 n = 4 - 9 10 11 12 ; - 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要
从其内部最⼤元素大⼩的整数倍地址开始存储
。(struct a⾥存有struct b,b⾥有char、int、double等元素,那b应该从8的整数倍开始存储。) - 收尾⼯作:结构体的总⼤⼩,也就是
sizeof
的结果,必须是其内部最⼤成员的整数倍
,不⾜的要补⻬
。
2.案例分析结构体内存对齐
结合下面的案例进行分析,两个结构体Struct1和Struct2
,属性类型都一样,只是char
和int
的顺序不一样的,根据运算符sizeof
获取的两个结构体的大小一样吗?
很意外,不一样,为什么呢?结合上面的结构体内存对齐的原则来分析
:
1. 结构体-Struct1
- double a: 第一个成员,8字节,从0开始,
占用位置:0-7
; - char b: 1个字节,从8开始,也是1的整数倍,
占用位置:8
; - int c: 4个字节,从9开始?不对,从12开始,
占用位置:12 13 14 15
; - short d: 2个字节,从16开始,也是2的整数倍,
占用位置:16 17
;
结合8字节对齐
原则,最终大小24。
2. 结构体-Struct2
- double a: 第一个成员,8字节,从0开始,
占用位置:0-7
; - int b: 4个字节,从8开始,也是4的整数倍,
占用位置:8 9 10 11
; - char c: 1个字节,从12开始,
占用位置:12
; - short d: 2个字节,从13开始?不对,从14开始,
占用位置:14 15
;
结合8字节对齐
原则,最终大小16。
那么我们在开发中是否需要注意类的参数的顺序呢,因为类也是结构体,类是不是也有这样情况呢?不需要!因为类是OC对结构体的一层封装,做了一些内存优化
,比如我们上面提到的内存重排
。而结构体有点傻^_^!
附一张C、Objc-C内存占用表:
3.结构体嵌套案例解析
下面两个结构体,Struct1、Struct3
,其中Struct3
中嵌套了一个Struct1
,如果使用运算符sizeOf
获取Struct3
的大小,应该输出多少呢?
struct Struct1 {
double a;
char b;
int c;
short d;
} struct1;
struct Struct3 {
double a;
int b;
char c;
short d;
int e;
struct Struct1 str;
} struct3;
结合上面的原则,先手动算一下:
- double a:第一个成员,8字节,从0开始,
占用位置:0-7
; - int b:4个字节,从8开始,
占用位置:8 9 10 11
; - char c:1个字节,从12开始,
占用位置:12
; - short d:2个字节,从13开始?不对,从14开始,
占用位置:14 15
; - int e:4个字节,从16开始,
占用位置:16 17 18 19
;
Struct1 str:Struct1中最大的是a,占8个字节,位置需要是8的整数倍
,所以从24开始;
- double a:
占用位置:24-31
; - char b:1个字节,从32开始,
占用位置:32
; - int c:4个字节,从36开始,
占用位置:36 37 38 39
; - short d:2个字节,从40开始,
占用位置:40 41
;
结合8字节对齐
原则,最终大小48。
结果对不对呢?验证一下,完全正确:
4.为什么要内存对齐
cpu在存取数据时,并不是以字节为单位,而是以块为单位存取,当读取到下面这段特殊的组合内存区域时,8字节读取肯定是无法解析的,而如果以字节为单位进行读取会极大的影响性能,效率太低!
在读取这段特殊的组合内存区域时,根据已知的数据组合,里面最大的4个字节空间去读,需要读取两次,并且需要将两次的数据拼接后才能获取int。
如果采用内存对齐原则,就可以一次性完整的读取一个int数据。
提高了读取效率!
五.malloc分析探索
在进行malloc分析探索之前,先引入一个案例:
结果分析:
- sizeof:是一个运算符,获取的是类型的大小(int、size_t、结构体、指针变量等)——>
lg1指针类型,所以返回8字节
; class_getInstanceSize:是一个函数(调用时需要开辟额外的内存空间),程序运行时才获取,计算的是类的大小(至少需要的大小)——>
属性、成员变量
- 创建的对象【至少】需要的内存大小
- 其中class_getInstanceSize,源码核心
(x + WORD_MASK) & ~WORD_MASK;
8字节对齐。 - 不考虑
malloc函数
的话,内存对齐一般是以【8】对齐 #import <objc/runtime.h>
malloc_size:堆空间【实际】分配给对象的内存大小——>
16字节对齐
- 在Mac、iOS中的malloc函数分配的内存大小总是【16】的倍数
#import <malloc/malloc.h>
1.malloc分析探索思路
calloc方法
,作为alloc流程
中的核心步骤之一,主要功能是为对象分配内存空间
,并返回指向该内存地址的指针
。通过前面的学习,我们知道在instacnceSize方法
里计算出了对象需要申请的内存大小
,那系统为对象实际分配的内存大小
和需要申请的内存大小
是一样吗?
准备一份libmalloc源码
。运行下面的一段程序:
void *p = calloc(1, 24);
NSLog(@"%lu", malloc_size(p));
申请24,按照16字节对齐原则,实际开辟控件应该是32字节!也就是说malloc_size(p)应该等于32。
运行结果和猜想是一致的32,那么底层是不是这样处理的呢?探究一下!
跟踪代码运行到malloc_zone_calloc方法
,根据返回值,判断关键代码是1441行。
继续运行程序,根据total_bytes<=NANO_MAX_SIZE的判断,正常流程是不会大于最大值的
,所以886行是关键代码。
我们所关心的点是大小的设置,所以判断621行位关键流程。
在这个方法里找到了16字节对齐算法
,也就是系统实际开辟的内存空间的大小
。其中NANO_REGIME_QUANTA_SIZE = 16,SHIFT_NANO_QUANTUM = 4
,所以k = (24 + 16 - 15) >> 4;之后再左移4位,16字节对齐
!
2.内存申请和内存分配总结
alloc
创建对象时,系统会先经过instanceSize方法
计算需要申请多大的内存空间
(已默认16字节对齐),再在calloc方法
里对申请的内存大小进行16字节对齐
处理,然后按处理后的结果来给对象分配内存空间,并返回内存地址。系统最终实际为对象分配的内存空间大小为16字节的整数倍
,并且最少16字节
,如果instanceSize方法
里是按16字节对齐
的,那实际分配的内存大小和申请的内存大小相同;如果是按8字节对齐
,则不同。^_^