[TOC]
在探索OC对象原理之前,我们首先需要了解以下知识点
lldb是xcode自带的命令行调试工具。
我们可以通过:
expression可简写为expr或者e expression命令的作用是执行一个表达式,并将表达式返回的结果输出。expression的完整语法是这样的: expression
:命令选项,一般情况下使用默认的即可,不需要特别标明。--
: 命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,--可以省略
: 要执行的表达式例如
计算以及生成一个表达式
(lldb) expr (int)printf ("Print nine: %d.\n", 4 + 5)
Print nine: 9.
(int) $0 = 15
创建一个变量并分配值,注意这里的变量需要添加$前缀
(lldb) expr int $val = 10
(lldb) expr $val
(int) $val = 10
格式化相关打印
(lldb) e -f bin -- 10
(int) $22 = 0b00000000000000000000000000001010
(lldb) e -f oct -- 10
(int) $23 = 012
其中:
p是expression --
的简写,它的工作是把接收到参数在当前环境中进行编译,然后打印出来。 例如: 使用p指令做进制转换
//默认打印为10进制
(lldb) p 10
(int) $0 = 10
//转16进制
(lldb) p/x 10
(int) $1 = 0x0000000a
//转8进制
(lldb) p/o 10
(int) $2 = 012
//转二进制
(lldb) p/t 10
(int) $3 = 0b00000000000000000000000000001010
//字符转10进制数字
(lldb) p/d 'A'
(char) $4 = 65
//10进制数字转字符
(lldb) p/c 66
(int) $5 = B\0\0\0
复制代码
总结: p是expression --
的简写,它的工作是把接收到参数在当前环境中进行编译,然后打印出来。po是expression -o --
的简写,其中-o的表示:-o ( --object-description ),它所做的操作和p相同。如果接收到的参数是一个指针,那么它会调用对象的description方法并打印;如果接收到的参数是一个core foundation对象,那么它会调用CFShow方法并打印。如果这两个方法都调用失败,那么po打印出和p相同的内容。
x/nuf address 内存读取指令
例如如下指令: x/4gx
指令: 意思就是将内存每8字节分成1段,一共4段,然后以16进制的形式输出出来
上面
0x100c25360: 3d 83 00 00 01 80 1d 01 00 00 00 00 00 00 00 00 =...............
0x100c25370: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
表示直接读取p1变量在内存中的地址,其中0x100c25360是起始地址,之后的3d 83 00 00 01 80 1d 01
每2位占一个字节(16进制),一共占用了8个字节,后面的00 00 00 00 00 00 00 00
表示该内存中是空内容。注意这里的0x100c25360
是一个地址编号,就是计算机内部的最小存储单元,一个字节。而它存储的内容每2位占一个字节。
另外3d 83 00 00 01 80 1d 01
这块地址,从后往前拼接到一块就是0x011d80010000833d
这与通过x/4gz p1
打印的0x011d80010000833d
是相同的。
我们除了通过上面在控制台输入lldb命令查看内存外,我们还可以借助界面话工具查看:
博客推荐: https://juejin.cn/post/6971358469949489183/ https://juejin.cn/post/6972203893925085192
字节对齐主要是为了提高内存的访问效率。比如intel 32位cpu,每个总线周期都是从偶地址开始读取32位的内存数据,如果数据存放地址不是从偶数开始,则可能出现需要两个总线周期才能读取到想要的数据,那就大大降低了访问效率,因此需要在内存中存放数据时进行对齐。 通常我们说字节对齐很多时候都是说struct结构体的内存对齐。 内存对齐主要遵循下面三个原则:
我们再来看下面实例:
结构体struct1和struct2内部变量完全一样,只是char和int变量的顺序不一样,为什么sizeof不一样呢?这里就是因为内存对齐造成的。
按照内存对齐原则,我们来分析一下struct1和struct2的内存分配过程,假设内存起始地址为0x0000
struct ZBStruct1 {
double a; // 8字节 内存分配 [0, 7]
char b; // 1字节 内存分配 [8, 11] 正常分配[8]
int c; // 4字节 内存分配 [12, 15] 9,10,11因为不能被4整除,所以不满足内存对齐,
short d; // 2字节 内存分配 [16, 24] 正常分配为[16,17] 因为17不能被8整除,不满足内存对齐,一直往后补充字节到24,可以被8整除
} struct1;
struct ZBStruct2 {
double a; // 8字节 内存分配 [0, 7]
int b; // 4字节 内存分配 [8, 11]
char c; // 1字节 内存分配 [12]
short d; // 2字节 内存分配 [14, 16]
} struct2;
通过以上分析可以得到与打印结果相同的数据。我们接下来验证一下:
通过上图我们可以发现
按照我们上面分析的过程,struct1一共分配24字节,验证正确。 不过这里还有一个问题,按照我们的分析sturct1应该先分配b变量,再分配c变量,但是实际存储0x0000000200000061
却把c===2===0x00000002,b==='a'===00000061交换了顺序,这是为什么呢? 这里的原因我们通过x/4gx读取内存的时候是从后往前读取的,也就是实际内存中是 61 00 00 00 02 00 00 00这样的,实际上还是先存储的b,再存储的c
总计分配16字节
结论: 1. 结构体的对齐是按照变量先后顺序依次进行对齐的 2. 如果结构体中包含子结构体,那么子结构体同样需要满足结构体的对齐原则,需要先对齐之后,再对齐父结构体
实例1:
struct Struct1{
double a;
char b;
int c;
short d;
} s1;
struct Struct2{
long a;
int b;
short c;
char d;
struct Struct1 s1;
} s2 = {
1, 2, 3, 'a',
{10.0, 'd', 11, 12}
};
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"Struct2 Size = %ld", sizeof(s2));
}
return 0;
}
2022-03-24 13:21:44.310610+0800 001-内存对齐原则-Demo[89487:7180330] Struct2 Size = 40
接下来我们分析一下Struct2的内存对齐过程:
s2内存分配如下:
验证通过
实例2: 我们对实例1的变量顺序调整一下
struct Struct1{
double a;
char b;
int c;
short d;
} s1;
struct Struct2{
long a;
int b;
short c;
struct Struct1 s1;
char d;
} s2 = {
1, 2, 3,
{10.0, 'd', 11, 12},
'a'
};
int main(int argc, char * argv[]) {
NSLog(@"Struct2 Size = %ld", sizeof(s2));
}
2022-03-24 13:49:59.159520+0800 001-内存对齐原则-Demo[89988:7202471] Struct2 Size = 48
接下来我们分析一下Struct2的内存对齐过程:
s2内存分配如下:
实例3: 我们对实例2的变量顺序再调整一下
struct Struct1{
int a;
double b;
char c;
char d;
} s1;
struct Struct2{
struct Struct1 s1;
char a;
long b;
int c;
short d;
} s2 = {
{10, 11.0, 'a', 'b'},
'c',1, 2, 3
};
int main(int argc, char * argv[]) {
NSLog(@"Struct2 Size = %ld", sizeof(s2));
}
2022-03-24 13:49:59.159520+0800 001-内存对齐原则-Demo[89988:7202471] Struct2 Size = 48
接下来我们分析一下Struct2的内存对齐过程:
s2内存分配如下:
我们在Xcode中经常会看到如下编译选项 可能很多小伙伴不知道这是个什么东西。 这其实就是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器Apple clang
简单讲,编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)
高级计算机语言便于人编写,阅读交流,维护。机器语言是计算机能直接解读、运行的。编译器将汇编或高级计算机语言源程序(Source program)作为输入,翻译成目标语言(Target language)机器代码的等价程序。源代码一般为高级语言 (High-level language), 如Pascal、C、C++、Java、汉语编程等或汇编语言,而目标则是机器语言的目标代码(Object code),有时也称作机器代码(Machine code)。
传统的编译器通常分为三个部分,前端(frontEnd),优化器(Optimizer)和后端(backEnd)。在编译过程中,前端主要负责词法和语法分析,将源代码转化为抽象语法树;优化器则是在前端的基础上,对得到的中间代码进行优化,使代码更加高效;后端则是将已经优化的中间代码转化为针对各自平台的机器代码。
在苹果发展的历程中,先后使用了GCC、LLVM GCC、LLVM compliler 2.0等编译器
GCC(GNU Compiler Collection,GNU编译器套装),是一套由 GNU 开发的编程语言编译器。它是一套以 GPL 及 LGPL 许可证所发行的自由软件,也是 GNU计划的关键部分,亦是自由的类Unix及苹果电脑 Mac OS X 操作系统的标准编译器。
GCC 原名为 GNU C 语言编译器,因为它原本只能处理 C语言。GCC 很快地扩展,变得可处理 C++。之后也变得可处理 Fortran、Pascal、Objective-C、Java, 以及 Ada与其他语言。
LLVM 是 Low Level Virtual Machine 的简称,这个库提供了与编译器相关的支持,能够进行程序语言的编译期优化、链接优化、在线编译优化、代码生成。简而言之,可以作为多种语言编译器的后端来使用。LLVM属于编译器的中间层,它的输入是编译器的IF代码,输出经过最佳化的IF代码。然后再被编译器转化为机器相关的汇编代码。
苹果llvm官方源码 https://github.com/apple/llvm-project
Apple一直使用GCC作为官方的编译器。GCC作为开源世界的编译器标准一直做得不错,但Apple对编译工具会提出更高的要求。慢慢的GCC无法满足Apple编译器的需求,于是Apple请来了编译器高材生Chris Lattner,他对LLVM 的链接优化被直接加入到 Apple 的代码链接器上,而 LLVM-GCC 也被同步到使用 GCC4.0 代码。
再后来,随着各种条件的限制,Apple无法使用LLVM 继续改进GCC的代码质量。于是,Apple决定从零开始写 C、C++、Objective-C语言的前端 Clang,完全替代掉GCC。于是clang编译器诞生。
Clang是⼀个C语⾔、C++、Objective-C语⾔的轻量级编译器。源代码发布于BSD协议下。Clang是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器
下面这张图将显示GCC、LLVM-GCC、LLVM Compiler这三个编译选项的不同点:
Clang 特性:
GCC 优势:
目前苹果推荐使用clang作为xcode的编译器。
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。 例如在存放一个开关量时,只有 0 和 1 两种状态,用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。
位域的定义和结构体有些相似,其一般形式为:
struct struct_name
{
位域列表 //格式为:[类型说明符 位域名:位域长度]
} name;
例如下面这样定义一个位域:
struct bits
{
int a:8;
int b:2;
int c:6;
}data;
上述位域,说明 data 为 struct bits 变量,共占两个字节,16位。其中位域 a 占 8 位,位域 b 占 2 位,位域 c 占 6位
位域有以下特点:
struct bits
{
int a:4
int :0 /*空域*/
int b:4 /*从下一单元开始存放*/
int :4// 该4位不能使用
}
以上,a 占第一字节的 4 位,后 4 位填 0 表示不使用,b 从第二字节开始,占用 4 位,后4位不能使用。假设int是4字节,也就是32位,也就是位域a最大支持32,也就是 int a : 32
我们举个例子:
上面例子中,结构体s11占用4字节,位域s22占用1字节,位域大大节省了内存空间。
位域在本质上就是一种结构类型,不过其成员是按二进位分配的。
联合体类型的一般形式为:
union 联合体类型名
{
成员类型 联合体成员名1;
成员类型 联合体成员名2;
...
成员类型 联合体成员名n;
}
union 是定义联合体数据类型的关键字,联合体类型名是一个标识符,该标识符以后就是一个新的数据类型,成员类型是常规的数据类型,用来设置联合体成员的存储空间。
定义联合体有如下几种方式:
先定义联合体,然后声明联合体变量
union MyUnion
{
int a;
char b;
float c;
};
union MyUnion myUnion;
可以直接在定义时声明联合体变量
union MyUnion
{
int a;
char b;
float c;
}myUnion;
可以直接声明联合体变量(该方式省略了联合体类型名)
union
{
int a;
char b;
float c;
}myUnion;
联合体的初始化方式和结构体相同,但联合体只能初始化一个值。尽管联合体中有多个成员变量,但是却是多个成员共用一个存储空间。
union MyUnion
{
int a;
char b;
float c;
}myUnion = {'A'};
这种方式赋值可能存在问题,因为不能确定赋的值到底赋给了哪个变量,推荐使用下列方式:
union MyUnion
{
int a;
char b;
double c;
}myUnion;
myUnion.b = 'A';
还是上面的例子,我们通过sizeof计算myUnion每个变量的大小,可以得出: a的大小是4字节,b的大小是1字节,c的大小是8字节,myUnion整体的大小是8字节。 联合体所有的成员共用一个存储空间,联合体存储空间的大小取决于最大成员的大小。
我们再举个例子: 我们仔细观察上面当一个联合体的变量被赋值的时候,其他变量的情况。
对比结构体与联合体我们可以发现如下特点:
通过联合体,然后结合位域,能够进一步节省内存空间。看如下实例
union u1 {
unsigned long bits;
Class cls;
struct {
unsigned long a : 1;
unsigned long b : 1;
unsigned long c : 1;
unsigned long d : 44;
unsigned long e : 6;
unsigned long f : 1;
unsigned long g : 1;
unsigned long h : 1;
unsigned long i : 8;
};
}myUnion;
执行myUnion.cls = person.class;
由上面的分析我们可以得出如下结论:
所谓的大端模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中; 所谓的小端模式,是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。
为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
我们的ios设备就是采用的小端模式。
例如,32bit宽的数0x12345678在小端模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:
内存地址 | 0x4000 | 0x4001 | 0x4002 | 0x4003 |
---|---|---|---|---|
存放内容 | 0x78 | 0x56 | 0x34 | 0x12 |
而在大端模式CPU内存中的存放方式则为:
内存地址 | 0x4000 | 0x4001 | 0x4002 | 0x4003 |
---|---|---|---|---|
存放内容 | 0x12 | 0x34 | 0x56 | 0x78 |