OC底层探索之对象原理(上)

OC对象的创建

我们经常使用allocinitnew 来创建对象,它们有什么区别呢

直接上代码

LGPerson *p1 = [LGPerson alloc];
LGPerson *p2 = [p1 init];
LGPerson *p3 = [p1 init];
        
NSLog(@"p1---%@ - %p - %p", p1, p1, &p1);
NSLog(@"p2---%@ - %p - %p", p2, p2, &p2);
NSLog(@"p3---%@ - %p - %p", p3, p3, &p3);

打印结果如下:


打印结果

首先解释一下

  • %@打印的是对象
  • %p打印的指针地址
  • %p - &p打印的是指向对象内存的指针地址
    由此可见
  • 第一列: p1p2p3相等,对象都是0x100b27a40
  • 第二列:p1p2p3相等,对象的指针地址都是0x100b27a40,指向同一内存空间,说明了我们创建对象其实只用alloc就可以了,不用再init
  • 第三列: &p1, &p2, &p3 不相等, 打印的是指针地址, obj1obj2obj3三个不同指针, 地址不同

alloc探索

为什么创建对象只用alloc,不用再init,就可以创建一个对象?那我们就基于objc4-838(macOS 12.3 Xcode 13.2)进行探索一下,看系统具体是怎样实现一个对象的创建的。

下载objc4-838之后打开工程,选择SXObjcDebug工程


选择工程

打开main文件,点击[LGPerson alloc]方法


点击alloc方法

进入[NSObject alloc]方法


[NSObject alloc]函数

依次进去,并打上相应流程断点
_objc_rootAlloc函数
callAlloc函数
_objc_rootAllocWithZone函数
_class_createInstanceFromZone函数

好了,我们开始运行项目,需要注意的是运行项目之前需要把打得断点取消掉,因为main函数之前系统也会创建很多的对象,我们只需要研究[LGPerson alloc]这个函数的具体实现就可以了。
首先定位到[LGPerson alloc]这行代码,恢复刚打得断点。


定位[LGPerson alloc]

具体的调用流程依次是这样的


alloc调用流程

我们发现先走了这段代码callAlloc里面的
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));给这个class发了方法为alloc的一个消息,
打开函数调用栈,发现有一个objc_alloc函数,很显然是之前没用调用[NSObject alloc]这个方法的
WeChate5b8d7e9f91d943474bbb61480375816.png

搜索源码直接打上个断点,再运行,发现首先运行了objc_alloc方法,然后依次执行


image.png

image.png

alloc


image.png

_objc_rootAlloc
image.png

callAlloc -> _objc_rootAllocWithZone
image.png

_objc_rootAllocWithZone->_class_createInstanceFromZone


image.png

_class_createInstanceFromZone
image.png

这样就总结出来了底层alloc的调用流程
alloc的调用流程.png

使用符号断点探索alloc
添加符号断点,依次加入objc_alloc, callAlloc, [NSObject alloc],_objc_rootAlloc, _objc_rootAllocWithZone, _class_createInstanceFromZone等。注意main函数之前生成的对象也会走相应的方法,要先把这些断点disable,断点到LGPerson *p1 = [LGPerson alloc];


image.png

需要注意的是alloc方法是一个类方法,加断点的是需要把类名加上,如[NSObject alloc]


[NSObject alloc]断点.png

开跑,函数调用顺序如下
objc_alloc

callAlloc

alloc

_objc_rootAlloc.png

callAlloc.png

_objc_rootAllocWithZone.png

_class_creatInstanceFromZone.png

可以看到_class_creatInstanceFromZone方法内部是返回的是一个obj的也就是一个NSObject对象

init方法

打开源码,init函数返回来_objc_rootInit,_objc_rootInit返回obj,看得出来,init方法没有进行任何操作,这一点苹果用到了工厂模式,当我们需要自定义某个类时,可以通过重写init方法来进行自定义操作。


init函数

_objc_rootInit函数

编译器优化

编译器优化说明

  • -O0 关闭所有优化 代码空间大,执行效率低
  • -O1 基本优化等级 编译器在不花费太多编译时间基础上,试图生成更快、更小的代码
  • -O2 O1的升级版,推荐的优化级别 编译器试图提高代码性能,而不会增大体积和占用太多编译时间
  • -O3 最危险的优化等级 会延长代码编译时间,生成更大体积、更耗内存的二进制文件,大大增加编译失败的几率和不可预知的程序行为,得不偿失
  • -Og O1基础上,去掉了那些影响调试的优化 如果最终是为了调试程序,可以使用这个参数。不过光有这个参数也是不行的,这个参数只是告诉编译器,编译后的代码不要影响调试,但调试信息的生成还是靠 -g 参数的
  • -Os O2基础上,进一步优化代码尺寸 去掉了那些会导致最终可执行程序增大的优化,如果想要更小的可执行程序,可选择这个参数。
  • -Ofast 优化到破坏标准合规性的点(等效于-O3 -ffast-math ) 是在 -O3 的基础上,添加了一些非常规优化,这些优化是通过打破一些国际标准(比如一些数学函数的实现标准)来实现的,所以一般不推荐使用该参数。

举个例子

int sum (int a , int b) {
    return a + b;
}

在main函数里面调用该sum函数,使用----分割sum函数调用前后。


编译器优化示例代码

运行打开汇编模式,我们会看到3和4的值


编译器优化示例代码

打开Build Setting在Debug模式下系统默认是O0是不优化的
修改优化等级

修改Debug和Release模式的一样的优化等级Os


image.png

再运行一次,发现编译器把sum函数也优化掉了

编译器优化之后

对象的内存对齐方式

alloc探索时 _class_createInstanceFromZone返回一个NSObject对象,我们看到这里面有一个instanceSize函数,它是给对象计算需要的内存空间的。


instanceSize函数

cmd+instanceSize点击 依次进入


对象需要内存空间

对象需要内存空间

看到了这个内存对其算法,在64位的架构下WORD_MASK为7,在31位的架构下WORD_MASK为3。

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

在64位架构下,假设一个对象实际内存是10,即x=10,
x + WORD_MASK = 10 + 7 = 17 = 10001
~WORD_MASK = 17~7 = 11000
(x + WORD_MASK) & ~WORD_MASK = 10001 & 11000 = 10000 = 16

也可以
(x + WORD_MASK) >> 3 << 3 = 10001 >> 3 << 3 = 10 << 3 = 10000 = 16
所以这个对象实际需要的内存大小就是16,苹果是以8字节对齐来计算对象的内存大小的。这样就可以减少CPU的开销,以空间换取时间。

结构体内存对齐

原则:

  • 数据成员对⻬规则:结构(struct)的第一个数据成员放
    在offset为0的地方,以后每个数据成员存储的起始位置要从
    该成员大小或者成员的子成员大小的整数倍开始(比如int为4
    字节,则要从4的整数倍地址开始存储)。
  • 结构体作为成员:如果一个结构里有某些结构体成员,则结
    构体成员要从其内部最大元素大小的整数倍地址开始存
    储.(struct a里存有struct b,b里有char,int ,double
    等元素,那b应该从8的整数倍开始存储)。
  • 收尾工作:结构体的总大小,也就是sizeof的结果必须是
    其内部最大成员的整数倍,不足的要补⻬。
    数据类型占的字节数:


    数据类型占的字节数.png

举例:
double:8字节 从0位置开始需要8位,目前到7号位置
char:1字节 从8号位置开始存储,需要1字节,到8号位置
int:4字节 从9号位置开始存储,需要4字节,9不是4的倍数,所以从12号位置开始存储,到15位置
short:2字节 从16号位置开始存储,需要2字节,到17号位置
因为该结构体内的成员最大是double是字节,17不是8的倍数,所以需要32字节的内存。

struct LGStruct1 {
    double a; // 8字节 [0 7] 从0到7
    char b; // 1字节 [8]
    int c; // 4字节 [12 13 14 15]
    short d; // 2字节 [16 17] 所以需要 24字节的内存
}struct1;

验证一下,打印结果是24,正确


验证struct1结构体内存

调整一下结构体内部成员的顺序
double:8字节 从0位置开始需要8位,目前到7号位置
int:4字节 从8号位置开始存储,需要4字节,到11位置
char:1字节 从12号位置开始存储,需要1字节,到12号位置
short:2字节 从13号位置开始存储,需要2字节,13不是2的倍数,所以从14号位置开始存储,到15位置
因为该结构体内的成员最大是double是字节,15不是8的倍数,所以需要16字节的内存

struct LGStruct2 {
    double a; // 8字节 [0 7] 从0到7
    int b; // 4字节 [8 9 10 11]
    char c; // 1字节 [12]
    short d; // 2字节 [14 15] 所以需要 16字节的内存
}struct2;

验证一下,打印结果是16,正确


验证struct2结构体内存

结构体内嵌结构体,分析LGStruct3需要64字节

struct LGStruct3 {
    double a; // 8字节 [0 7] 从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 struct1; // 24字节 [24 47]
    struct LGStruct2 struct2; // 16字节 [48 63]
}struct3;

验证一下,打印结果是64,perfect


验证struct3结构体内存

你可能感兴趣的:(OC底层探索之对象原理(上))