[TOC]
前言
- 本文主要围绕以下几点内容展开讨论;
对象的分配一定会占用8个字节吗?如果它是char类型或者int类型的,占不满8字节怎么办,系统会不会对内存分配进行优化?
属性的字节对齐和对象的字节对齐有何不同?
isa是个啥?里面包含啥?
共同体、位域的原理?
或运算的原理;
OC内存优化
探究字节对齐
举个例子:
- int 4字节;
- char 1字节;
- nsstring 8字节;
思考
:由于字节对齐的原则
(不满足8字节就要补齐),如果一个对象中都是int这样的属性那是不是要开辟很多个8字节的内存空间,这样是不是很浪费?
首先还是要搞清楚,什么是字节对齐和字节补齐!请看下面;
- 该对象暴漏
2
个属性,按照字节对齐的原则,int类型只占了4
个字节,需要补齐为8
个字节,所以该对象目前有三个属性isa、name、age
,所以内存为该对象开辟了24
个字节;
- 该对象暴漏
3
个属性,按照字节对齐的原则,int类型只占了4
个字节,需要补齐为8
个字节,但是这个对象的下个属性是ch,ch本身占了1
个字节,系统默认将这一个字节排列到了age的后面,节约的空间,所以该对象目前有四个属性isa、name、age、ch
,所以内存为该对象也是开辟了24
个字节;
通过输出内存值的方式来验证字节对齐和字节补齐的原理,请看下面;
-
C语言的结构体浪费内存空间
,它是按照写入结构体的顺序依次去开辟内存空间的,也就造成了下图的情况,同样的结构体
,一个开辟了24字节
、另一个开辟了16字节
;
-
OC语言对象中的属性并不会因为属性排列的先后顺序,而导致内存分配时造成的浪费
,通过debug调试 在源码size_t size = cls->instanceSize(extraBytes)
;出输出两个对象的size均为32
字节;
探究class_getInstanceSize
-
注意!:
这里要注意一点,在oc中输出类的size直接使用NSLog(@"%lu",sizeof(p));是不准确的,因为它不能把类中所有的元素都计算出来。举个例子;
-
疑问
:那如何打印系统实际为我们类开辟的的内存空间
是多大呢?请看下图;
使用class_getInstanceSize
接口直接获取内存大小;
-
疑问
:那这样打印的值就是最终系统为我们开辟的内存大小吗?请看下图;
当对象中所有元素(包括isa
)所需要开辟的内存空间<16时,这时使用class_getInstanceSize
输出的内存大小是要小于16字节的;
上面已经说了系统为我们类开辟内存大小可以用
class_getInstanceSize
来输出,那我们类最开始希望内存为我们开辟多少内存
用什么来展示呢?请看下图;
结论:class_getInstanceSize返回类对象至少需要多少空间
和malloc_size()返回的是实际分配的内存空间
是不一致的!!!!
探究malloc_size
探究
malloc_size
方法;
- 1、找到malloc_size方法;
alloc_size
的调用是在object-runtime_new.mm
中进行的,如下图;但是alloc_size
的实现文件并没暴漏出来,所以我们还是要去官网下载malloc_size
的源码;
- 2、找到malloc_size源码;源码传送门
- 3、debug源码(1);
calloc
-->malloc_zone_calloc
-->ptr = zone->calloc(zone, num_items, size);
疑问?:
我擦?calloc
执行到最后还是calloc
怎么递归了?
- 3、debug源码(2);
通过箭头函数(属性函数:属性函数主要的目的是赋值,他的实现不在这里)
找到这个函数实现的方法请看下图;
- 3、debug源码(3);
- 4、debug源码(4)-->
16字节对齐!
;
-
重点!!!:
这里我们再回顾下16字节对齐和8字节对齐的原理,16字节对齐就是 2^4-1
,8字节对齐就是2^3-1
- 5、debug源码(5)-->
16字节对齐!
;
这也就说明了为什么,传进来的size为40字节的内存空间会被系统修改为48,因为按照16字节对齐
的原则40需要补齐为48;
- 6、debug源码(6)-->
为啥参照对象就是8字节对齐、系统分配就是16字节对齐?
NSLog(@"class_getInstanceSize返回类对象至少需要多少空间为%lu",class_getInstanceSize([p class]));
NSLog(@"malloc_size()返回的是实际分配的内存空间%lu",malloc_size((__bridge const void *)(p)));
8字节对齐
---------参考的是对象当中的属性
16字节对齐
---------参考的是整个对象
- 7、debug源码(7)-->
这样就会造成一个现象,有可能至少需要的内存空间和实际分配的内存空间是不相同的
那为啥要多出来8位字节呢?
答案:
主要是为了对象之间的系统安全,因为你满足8字节对齐只能保证对象中属性之间的安全性,只有保证16字节对齐才能保证对象之间的安全性;系统开辟对象内存是连续的,满足16字节对齐原则,防止内存越界
;(请看下图)
探究编译器优化
思考?:
那系统这么厉害是怎么优化代码,让运行的代码达到优化的呢?
- 7、debug源码(7)-->从
Debug
切换到release
后系统做了什么?(此篇文章只简单介绍
)
编译代码
-->断点
-->always show disassembly
查看汇编源码会发现Debug模式下要比release模式下的代码多很多;这就说明在release模式下系统会将臃肿的代码进行整合和优化;
探究obj->initInstanceIsa(ISA)
1、探究ISA成员
isa是啥?:
ISA就是一个联合体!
联合体是干嘛的?
在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构,简称共用体,也叫联合体
。
举个联合体的例子!!!!!!!
注意:Union联合体的成员之间共享内存空间。以isa_t
为例,cls
成员和bits
成员虽然不同,但是两者的值实际在任何时候都是一致的。例如,isa.class = [NSString class]
指定了cls
指向NSString
类的内存地址,此时查看isa.bits
会发现其值为NSString
类的内存地址;反之,isa.bits = 0xFF
,则isa.class
的值变为255
。
isa
里面有啥?
cls
和bits
其中 struct
中的内容是为了解释bits
的8*8位;
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
那这个8字节的isa对象里面都放了啥?
2、位域,有以下特性
什么是位域?
开辟完的内存空间你指定那些属性放在哪个指定的区域,这就叫位域!
为什么引入位域的概念?
因为要节约内存空间!
因为一个字节有8位,如果你只用一个字节就可以放下这个成员的话,我就没必要开辟太大的内存给你用,所以就用位域的概念在每个位域放置不同的成员,然后通过移位的方式来达到取值的效果;
二进制重排
与oc内存优化
的原理类似;
联合体和位域
3、联合体union
也成共用体,有以下特性
- union中可以定义多个成员,
union的大小由最大成员的大小决定
. - union成员共享同一块大小的内存,一次只能使用其中的一个成员.
- 对union某一个成员赋值, 会覆盖其他成员的值.
- union的存放顺序是所有成员都从低地址开始存放的.
使用联合体的好处
- 多个成员共用一块内存
- 可读性强
- 使用位运算提高数据存储的效率
坏处
- 也是共用同一块内存, 有可能会浪费一定的空间
证明
先创建一个对象 LGTank
@interface LGTank(){
// 联合体
union {
char bits;
// 位域
struct {
char front : 1;
char back : 8;
char left : 1;
char right : 1;
};
} _direction;
}
1. union中可以定义多个成员, union的大小由最大成员的大小决定.
- struct中的值类型都为char时, 打印出来的大小为 1
bits 大小为1
结构体大小也为1
所以联合体大小为1
- 将struct其中一个成员变量类型改为 short, 打印出来的大小为 2
bits 大小为1
结构体大小为2
所以联合体大小为2
-
将bits类型改为 int, 打印出来的大小为 4
bits 大小为4
结构体大小为2
所以联合体大小为4 -
将struct其中一个成员变量类型改为 int, 打印出来的大小为 4
bits 大小为4
结构体大小为4
所以联合体大小为4
2. 联合体结构的分析
-
变量
char bits
这个使我们常见的定义变量的方式, 类型+变量名.
位域struct的分析
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
假如我们不知道有联合体和位域这个称谓, 这里定义了一个结构体, 没有声明结构体类型, 也没有创建结构体变量, 那放在这里有什么用呢?
-
打印联合体信息
可以看出来存在两个变量, 一个变量 bits, 一个匿名结构体变量, 值为(1, 0, 0, 0)
-
注释这段代码, 发现程序还是能够正常运行, 也还是能输出bits的值, 跟有没有注释这段代码前的结果一模一样.
但是这段代码输出的联合体重的变量只有一个 bits
3. 或运算和&运算原理
或运算有1得1;
0000 0000
0000 0001
或等于
0000 0001
- &运算有0得0、11得1;
0000 0000
1111 0110
或等于
0000 0000
4. 联合体的赋值
对结构体的赋值一般都是通过位运算进行赋值
#define LGDirectionFrontMask (1 << 0)
#define LGDirectionBackMask (1 << 1)
#define LGDirectionLeftMask (1 << 2)
#define LGDirectionRightMask (1 << 3)
- (void)setFront:(BOOL)isFront {
if (isFront) {
_direction.bits |= LGDirectionFrontMask;
} else {
_direction.bits &= ~LGDirectionFrontMask;
}
}
因为联合体共用的是一块内存, 所以不能直接对bits赋一个数值, 这样会导致结果超出预期范围之外.
需要通过对面罩Mask做位运算来进行赋值
需要设置front为YES,
比如 LGDirectionFrontMask 的二进制为 0x00000001
- 最后一位一定是1, 所以需要跟最后一位或运算
- 现在需要改变 front位的值, 而不影响到其他位的值, 所以其他为要跟00使用或运算
需要设置front为NO,
- 最后一位是0, 必须执行与运算
- 而执行运算的时候, 不影响其他位域的值, 则需要跟1进行或运算. 从而可以推断出跟bits进行与运算的数是0x11111110, 刚好等于~LGDirectionFrontMask
5. 共用空间
再来看这张图, 在设置属性的值的地方, 明明没有设置结构体中front的值, 可是输出的结果中却显示front=1, 就是我们设置的bits=1
猜想: 是否结构体中的值表示这个联合体的某一位的值?
#define LGDirectionFrontMask 0b0000011
尝试将面罩从0b1设置为0b11, 打印出来的结果如下
果然是表示这个联合体前两位的一个值
再次验证
#define LGDirectionFrontMask 0b0000101
猜测结果 应该是 front位和left位值为 1
输出的结果和我们预期是完全一模一样的.
所以, 我们可以知道这个结构体在联合体里面的作用相当于一个注释的功能, 就是告诉使用这个联合体的人, 这个联合体每一位对应存的是什么信息, 你要按照结构体中标注的位域空间来进行读取.
惊喜二, 上述验证除了解释联合体的特性之外, 还辅助我解释了小端序
这一特性
小端序
前面的笔记中说过小端模式
的特点是: 是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
当时一直不明白什么是数据的高字节/低字节, 什么又是内存的高地址/低地址
让联合体带着我们来看
#define LGDirectionFrontMask 0b00000101
LGDirectionFrontMask这个常量, 左边的是高字节, 这个很好理解, 不会理解的就当做十进制数的个十百千万
的顺序来理解就行.
而内存的低地址/高地址怎么理解呢? 我们平时通过x/4gx obj
来打印一个对象的属性内存段地址时, 可以看到属性的地址是依次增加的, 也就是说在前面的属性的地址属于低地址.
通过上面实例来分析: LGDirectionFrontMask最右边的1, 表示这个字节的最低字节了, 而对应的联合体union的内存地址中的 front 位--第一位, 也就是内存的最低地址, 从而验证了数据的低字节保存的内存的低地址中
nice, 学习总是环环相扣, 当你对某个知识点理不清楚的时候, 不要钻牛角尖, 去了解下一个知识点, 也许会柳暗花明又一村
联合体和结构体的对比
- 分别定义一个相同结构的联合体和结构体
// 联合体
union {
char bits;
// 位域
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
} _direction;
//结构体
struct {
char bits;
// 匿名结构体
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
} _word;
- 使用相同的面罩给他们赋值
#define LGDirectionFrontMask 0b0000101
- (void)setFront:(BOOL)isFront {
if (isFront) {
_direction.bits |= LGDirectionFrontMask;
_word.bits |= LGDirectionFrontMask;
} else {
_direction.bits &= ~LGDirectionFrontMask;
_word.bits &= ~LGDirectionFrontMask;
}
}
- 查看_direction和_word
(lldb) p tank->_direction
((anonymous union)) $0 = {
bits = '\x05'
= (front = '\x01', back = '\0', left = '\x01', right = '\0')
}
(lldb) p tank->_word
((anonymous struct)) $1 = {
bits = '\x05'
= (front = '\0', back = '\0', left = '\0', right = '\0')
}
在联合体中, 可以看到我们定义的那个匿名结构体中有值, 从右往左读分别是0x0101
, 而这个顺序恰好是bits值(5)的二进制值, 这便解释了位域
这个词的含义, 结构体所在内存的每一位的值.
而在结构体中, 可以看到属性bits
有值, 但是另一个属性匿名结构体中的值并没有任何变化, 说明这两个属性之间没有任何关联, 分别存在两个不同的地方.
如何判断ISA是cls还是 bits??
- 首先上面说了
cls
和bits
都属于联合体中的成员;那ISA它是哪种类型取决于什么呢?
答案
:取决于nonpointer是0还是1、如果是1代表是bits、如果是0则是cls;
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
总结
内存对齐的原则
- 原则一: 第一个数据成员放在offset为0的地方
- 原则二: 以后每个成员的起始位置为该成员大小的整数倍
- 原则三: 如果成员是结构体, 则这个成员的起始位置为结构体内部的最大成员的整数倍
- 原则四: 结构体总大小, 也就是sizeof的结果, 必须为内部成员(包括成员为结构体的内部成员)最大成员大小的整数倍, 不足要补齐.
内存对齐的思考
-
空间换时间
我们的内存按照8字节对齐, 计算机每次在读数据的时候, 每次按照8字节的刻度读取, 远比逐个字节读取的效率要高得多.
-
如何定义属性保证结构体所占字节最小
这个系统会自动编排, 不需要我们关心成员的排列方式. 系统是如何自动编排的, 有兴趣的时候可以研究下.
-
内存优化
内存对齐可以节约内存空间, 优化内存.
isa指针的结构也是优化内存的一种设计, 将不同的信息存在isa的不同位域, 避免使用过多的属性 -
二进制重排
先将有意义的内存排列在一起, 优先进行加载, 对没有用到的内存排列在其他地方等待加载, 以提高启动速度.
[图片上传失败...(image-bf64c7-1586161219748)]
属性的字节对齐和对象的字节对齐有何不同?
属性字节对齐8字节对齐
---------参考的是对象当中的属性
对象字节对齐16字节对齐
---------参考的是整个对象
isa是个啥?里面包含啥?
isa
是一个联合
体;它里面包含了两个属性;cls
和bits
,struct是对64位bits字节归属的解释;
联合体、位域的原理?他们搭配起来起到了什么作用?
或运算的原理;
- 或运算有1得1;
- &运算有0得0、11得1;