[TOC]
一、联合体 Union
1.1 联合体的特性
联合体 union
也成共用体,有以下特性:
- union中可以定义多个成员, union的大小由最大成员的大小决定.
- union成员共享同一块大小的内存,一次只能使用其中的一个成员.
- 对union某一个成员赋值, 会覆盖其他成员的值.
- union的存放顺序是所有成员都从低地址开始存放的.
使用联合体的好处
- 多个成员共用一块内存
- 可读性强
- 使用位运算提高数据存储的效率
使用联合体的坏处
- 也是共用同一块内存, 有可能会浪费一定的空间
1.2 联合体特性的验证
验证条件
- 先创建一个对象 LGTank
- 有一个
union
类型的属性_direction
- 结构体中有两个属性:
bits
和struct
(匿名结构体)
@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:union成员共享同一块大小的内存,一次只能使用其中的一个成员.
联合体结构的分析
-
变量
char bits
这个使我们常见的定义变量的方式, 类型+变量名.
-
位域
struct
的分析struct { char front : 1; char back : 1; char left : 1; char right : 1; };
假如我们不知道有联合体和位域这个称谓, 这里定义了一个结构体, 没有声明结构体类型, 也没有创建结构体变量, 那放在这里有什么用呢?
-
打印联合体信息
可以看出来存在两个变量, 一个变量 bits, 一个匿名结构体变量, 值为(1, 0, 0, 0)
-
注释这段代码, 发现程序还是能够正常运行, 也还是能输出bits的值, 跟有没有注释这段代码前的结果一模一样.
但是这段代码输出的联合体重的变量只有一个 bits
联合体的赋值(见下面)
-
联合体的赋值
对结构体的赋值一般都是通过位运算进行赋值
#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
共用空间
再来看这张图, 在设置属性的值的地方, 明明没有设置结构体中front的值, 可是输出的结果中却显示front=1, 就是我们设置的bits=1
猜想: 是否结构体中的值表示这个联合体的某一位的值?
#define LGDirectionFrontMask 0b0000011
尝试将面罩从0b1设置为0b11, 打印出来的结果如下
果然是表示这个联合体前两位的一个值
再次验证
#define LGDirectionFrontMask 0b0000101
猜测结果 应该是 front位和left位值为 1
输出的结果和我们预期是完全一模一样的.
所以, 我们可以知道这个结构体在联合体里面的作用相当于一个注释的功能, 就是告诉使用这个联合体的人, 这个联合体每一位对应存的是什么信息, 你要按照结构体中标注的位域空间来进行读取.
惊喜二, 上述验证除了解释联合体的特性之外, 还辅助我解释了小端序
这一特性
1.3 意外收获——小端序的理解
前面的笔记中说过小端模式
的特点是: 是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
当时一直不明白什么是数据的高字节/低字节, 什么又是内存的高地址/低地址
让联合体带着我们来看
#define LGDirectionFrontMask 0b00000101
LGDirectionFrontMask这个常量, 左边的是高字节, 这个很好理解, 不会理解的就当做十进制数的个十百千万
的顺序来理解就行.
而内存的低地址/高地址怎么理解呢? 我们平时通过x/4gx obj
来打印一个对象的属性内存段地址时, 可以看到属性的地址是依次增加的, 也就是说在前面的属性的地址属于低地址.
通过上面实例来分析: LGDirectionFrontMask最右边的1, 表示这个字节的最低字节了, 而对应的联合体union的内存地址中的 front 位--第一位, 也就是内存的最低地址, 从而验证了数据的低字节保存的内存的低地址中
nice, 学习总是环环相扣, 当你对某个知识点理不清楚的时候, 不要钻牛角尖, 去了解下一个知识点, 也许会柳暗花明又一村
1.4 联合体和结构体的对比
- 分别定义一个相同结构的联合体和结构体
// 联合体
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
有值, 但是另一个属性匿名结构体中的值并没有任何变化, 说明这两个属性之间没有任何关联, 分别存在两个不同的地方.
二、内存对齐
2.1 内存对齐的原则
- 原则一: 第一个数据成员放在offset为0的地方
- 原则二: 以后每个成员的起始位置为该成员大小的整数倍
- 原则三: 如果成员是结构体, 则这个成员的起始位置为结构体内部的最大成员的整数倍
- 原则四: 结构体总大小, 也就是sizeof的结果, 必须为内部成员(包括成员为结构体的内部成员)最大成员大小的整数倍, 不足要补齐.
2.2 内存对齐的思考
-
空间换时间
我们的内存按照8字节对齐, 计算机每次在读数据的时候, 每次按照8字节的刻度读取, 远比逐个字节读取的效率要高得多.
-
如何定义属性保证结构体所占字节最小
这个系统会自动编排, 不需要我们关心成员的排列方式. 系统是如何自动编排的, 有兴趣的时候可以研究下.
-
内存优化
内存对齐可以节约内存空间, 优化内存.
isa指针的结构也是优化内存的一种设计, 将不同的信息存在isa的不同位域, 避免使用过多的属性 -
二进制重排
先将有意义的内存排列在一起, 优先进行加载, 对没有用到的内存排列在其他地方等待加载, 以提高启动速度.
2.3 内存对齐的示例
// char 1
// short 2
// int 4
// double 8
// 原则一: 第一个数据成员放在offset为0的地方
// 原则二: 以后每个成员的起始位置为该成员大小的整数倍
// 原则三: 如果成员是结构体, 则这个成员的起始位置为结构体内部的最大成员的整数倍
// 原则四: 结构体总大小, 也就是sizeof的结果, 必须为内部成员(包括成员为结构体的内部成员)最大成员大小的整数倍, 不足要补齐.
struct LGStruct1 {
char a; // 1 -- 原则一 ==> a 所在下标为 0, 排位(0)
double b; // 8 -- 原则二 ==> 当前offset=1, 所以要 +7=8, b要从offset=8的地方开始排, 排位(8 9 10 11 12 13 14 15)
int c; // 4 -- 原则二 ==> 当前offset=16, 满足原则二, 直接排, 排位(16 17 18 19)
short d; // 2 -- 原则二 ==> 当前offset=20, 满足原则二, 直接排, 排位(20, 21)
// 原则四 ==> 当前size=22, 最大成员size=8, 不满足, 则需要补齐, 总大小为22+2=24
} myStruct1;
struct LGStruct2 {
double b; // 8 -- 原则一 ==> 当前offset=0, 排位(0 1 2 3 4 5 6 7)
int c; // 4 -- 原则二 ==> 当前offset=8, 满足原则二, 直接排, 排位(8 9 10 11)
char a; // 1 -- 原则二 ==> 当前offset=12, 满足原则二, 直接排, 排位(12 13)
short d; // 2 -- 原则二 ==> 当前offset=14, 满足原则二, 直接排, 排位(14 15)
// 原则四 ==> 当前size=16, 最大成员size=8, 满足原则三, 则总大小为16
} myStruct2;
struct LGStruct3 {
double b; // 8 -- 原则一 ==> 当前offset=0, 排位(0 1 2 3 4 5 6 7)
char a; // 1 -- 原则二 ==> 当前offset=8, 满足原则二, 直接排, 排位(8)
int c; // 4 -- 原则二 ==> 当前offset=9, 不满足原则二, 需补齐(9+3=12), 当前offset=12, 排位(12 13 14 15)
short d; // 2 -- 原则二 ==> 当前offset=16, 满足原则二, 直接排, 排位(16, 17)
struct LGStruct2 s;
// 16-- 原则三 ==> 当前offset=18, 不满足原则四, 结构体s里面最大成员b的倍数, 需补齐(18+6=24), 排位(24-39)
// 原则四 ==> 当前size=40, 最大成员size=8, 满足原则三, 则总大小为40
} myStruct3;
isa 本质
定位到isa指针
分析, 对于isa指针, 我们都知道它是用来表示一个对象是什么类型的, 可以猜到它和创建对象想关联.
从alloc
方法查找
-
alloc
-
通过上一篇关于alloc的探索知道我们最后都会调用callAlloc方法. 在这个方法中我们看到了两个关于
isa
的代码, 一个是调用一个isa方法, 一个是创建一个isa实例, 不管是哪一个, 我们只要找到isa这个指针的结构即可, 所以随便找个进入. 不妨使用根据initInstanceIsa
方法探寻
-
通过
initInstanceIsa
方法找到了isa的初始化方法
-
在
initIsa
方法中定位到isa_t newisa(0);
这行代码, 类型名是isa_t
, 变量名是newisa
, 那这段代码含义也显然意见了, 创建一个新的isa指针, 那么我们想要找的isa指针结构只差最后一步了
-
找到
isa_t
的定义, 是一个联合体, 联合体有什么特征了, 可以查看另一篇博客, 我们这里主要讲解isa_t
的类型
前面两行代码isa_t() { } isa_t(uintptr_t value) : bits(value) { }
分别表示isa_t的两个构造函数, 没多大考察意义--过
第四行代码
Class cls;
一个class属性, 看到这个, 我们应该要有一种豁然开朗的感觉, 为什么都说isa指针表示对象类型, 因为在这里.
后面的代码, 表示另一个重点, 为什么isa指针能表示那么多信息uintptr_t bits; #if defined(ISA_BITFIELD) struct { ISA_BITFIELD; // defined in isa.h }; #endif
位域宏定义ISA_BITFIELD
-
arm64架构
# if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL # define ISA_BITFIELD \ uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \ uintptr_t magic : 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t deallocating : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 19
-
x86架构
elif __x86_64__ # define ISA_MASK 0x00007ffffffffff8ULL # define ISA_MAGIC_MASK 0x001f800000000001ULL # define ISA_MAGIC_VALUE 0x001d800000000001ULL # define ISA_BITFIELD \ uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \ uintptr_t magic : 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t deallocating : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 8
- 因为我们真机都是arm64的架构, 所以下面对arm64结构的isa指针位域排列方式进行一个说明.
isa走向分析
我们都是站在巨人的肩上学习, 先来看一张巨人绘制的图
图中虚线部分, 非常清楚的描述出了 isa 指针的走向图, 但是我们可能还不太理解, 所以需要联合源码来一步一步证实.
验证思路
- 输出p中isa指针地址
x/g p
=>ptr - 输出
ptr&ISA_MASK
的值=>isa指向的对象obj的地址 - 输出这个地址所指向的对象
po $?
-
alloc一个LGPerson对象实例
LGPerson *p = [LGPerson alloc];
-
输出对象p的内存段地址: 我们知道第一个地址肯定表示isa
x/g p 0x1007071b0: 0x001d800100002421
打印对象的起始地址: 输出的是一个LGPerson对象
po 0x1007071b0 <LGPerson: 0x1007071b0>
而po isa指针, 输出的是这个0x001d800100002421的十进制值, 说明这里存的是一个值. 从上面
isa_t
的结构来看, 里面是有一个uintptr_t bits;
变量, 说明这个值就是isa.bits()
的值, 这个后面来证明.po 0x001d800100002421 8303516107940897
-
按照上述方式逐个打印isa指针所指向的类型
从这张图中可以看出来isa指针的走位图就是按照上述结构图中的位置
这张图再次验证了类对象在项目中只会存在一个