OC底层原理-objc 818(一)alloc&init&new原理分析

766 阅读7分钟

最新alloc源码调用流程

查看alloc调用底层的方法

通过查看汇编分析,然后在通过汇编分析的方法添加符号断点查看

先将在alloc初始化处添加断点

然后Debug->Debug Workflow->Always Show Disassembly来查看汇编中的调用

查看汇编,当前会停在我们当前要执行的函数,查看汇编发现,向下会调用_objc_alloc方法

此时我们在通过添加符号断点,将_objc_alloc添加

继续执行,我们发现确实执行了objc_alloc方法,接下来我们需要到objc源码中查看后面的流程

alloc及objc_alloc

NSObject类的初始化会直接调用_objc_alloc方法进行初始化,这是因为底层malloc类中进行了特殊处理,当调用alloc的时候会自动调用objc_alloc方法。

CustonClass自定义的类初始化时我们可以看到会走到alloc方法,是因为系统会先走一次NSObject的alloc也就是objc_alloc,在_objc_alloc的callAlloc中会重新初始化我们的自定义类,才会通过objc_msgSend给当前类发送alloc消息,这时其实才走到我们的alloc方法中。

查看objc源码

首先我们通过一个可以运行的objc项目来进行演示,可运行的objc项目在文章末尾自行下载

我们通过按commd+鼠标左键进入alloc的调用,发现就直接进入到了objc的alloc方法

alloc中调用_objc_rootAlloc

objc_rootAlloc继续调用callAlloc

callAlloc会调用_objc_rooAllocWithZone方法

_objc_rooAllocWithZone中会调用_class_createInstanFromZone方法进行内存分配和实例对象创建

_class_createInstanFromZone首先会根据传入对象的属性来计算内存大小,然后在创建一个size大小obj对象,最后将obj对象与cls也就是我们传入的类进行关联,最后将创建好的对象返回

内存对齐

计算机编码过程中内存分配是有一定规则的,一般长以8的倍数进行分配,但是iOS中比较特殊是以16的倍数进行分配的。

由于内存比较小,所以内存在分配时内存地址一般是连续的,内存读取的方式是以块状读取,那么当内存按照最小的块大小进行倍数分配时,当读取的时候可以防止读取出错。

各类型所需内存大小对照表

OC内存对齐计算方式

iOS对象都有一个isa指针,isa就是对象在内存的首地址,每个指针占8字节,但是对象还可能有自己的属性,那么初始的收我们就默认分配的最小内存就是16字节,而且内存是按16的倍数进行对齐。

下面我们查看源码:

如果计算出来的内存如小于16字节,那么直接返回16字节

内存对齐计算方式:x+size_t(15))&~size_t(15),将计算的内存大小与上一个按位取反的15

如计算出内存需要分配20那么内存对齐过程如下

20的二进制 0000 0000 0001 0100

15 的二进制 0000 0000 0000 1111 +

------------0000 0000 0010 0011

~15 二进制 1111 1111 1111 0000 &

------------0000 0000 0010 0000 == 32

所以会20字节内存对齐后会分配32字节

实质就是(X+15) >> 4 << 4:将我们计算出isa与属性内存大小结果加上15,然后在右移4位,目的是去除后四位,在左移4位来算出我们的内存大小,去除后四位的目的是保证计算出的结果都能是16的倍数

比如:

20+15 == 32

32 的二进制 0000 0000 0010 0011

->>4右移4位 0000 0000 0000 0010

-<<4左移4位 0000 0000 0010 0000 == 32就是我们实际要分配的内存大小

结构体内存对齐计算方式

  1. 数据成员对齐规则:结构体Struct或共用体Union的数据成员,第一个数据成员放在offset为0的地方,也就是第一个位置,以后每个数据成员存储的起始位置要能整除其成员的大小,如果不能整除起始位置需要后移到可以整除的位置(如:起始位置5 成员大小2,这时候就需要向后移动以为起始位置就是6,那么下一个成员的起始位置就要是8)。
  2. 结构体作为成员:如果一个结构体里有某些成员是结构体时,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(如:struct a中有struct b,b里有char int double等元素,但是b的实际大小是8,那么b在a中的起始位置就要是8的倍数才可以)
  1. 结构体的总大小需要是结构体中最大成员大小的整数倍,不足也时也要补齐
// 共占15字节 最大成员为8 内存对齐8的倍数 所以实际分配16字节
struct AA{
    double    a;   // 0~7   占8字节
    int       b;   // 8~11  占4字节
    short     c;   // 12~13 占2字节
    char      d;   // 14    占1字节
};


// 共占18字节 最大成员为8 内存对齐8的倍数 所以实际分配24字节
struct BB{
    double    a;   // 0~7   占8字节
    char      d;   // 8     占1字节
    int       b;   // 12~15 占4字节
    short     c;   // 16~18 占2字节
};

// 共占32字节 最大成员为16 内存对齐8的倍数 所以实际分配32字节
struct CC{
    double    a;   // 0~7   占8字节
    char      d;   // 8     占1字节
    struct AA b;   // 16~32 占16字节
};

init源码

通过源码查看,init返回的其实就是自身对象,不会创建新的对象

那么为什么还要有init方法呢?

init方法是我们对象初始化时的构造方法,开发者可通过重写init方法,来重构自己的构造方法,在初始化的时候可以带入自己自定义的参数来快速初始化对象。

new源码

我们可以看到,new实际上就是直接调用alloc后又帮我们调用了一次init方法

其实new防反就等同于alloc + init方法

LLDB常用命令

看完原理看代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        YJCar *car1 = [YJCar alloc];
        YJCar *car2 = [car1 init];
        YJCar *car3 = [car1 init];
        
        NSLog(@"%@--%p--%p", car1, car1, &car1);
        NSLog(@"%@--%p--%p", car2, car2, &car2);
        NSLog(@"%@--%p--%p", car3, car3, &car3);
    }
    return 0;
}

证明此时car1、car2、car3是同一个对象,都引用这car1

通过代码log打印方式

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        YJCar *car1 = [YJCar alloc];
        YJCar *car2 = [car1 init];
        YJCar *car3 = [car1 init];
        car1.name = @"奔驰";
        
        NSLog(@"%@--%@--%@", car1.name, car2.name, car3.name);
        car3.name = @"BMW";
        NSLog(@"%@--%@--%@", car1.name, car2.name, car3.name);
    }
    return 0;
}

通过窥探内存方式

(lldb) x car1
0x6000020ff780: e8 55 15 00 01 00 00 00 40 00 15 00 01 00 00 00  .U......@.......
0x6000020ff790: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
(lldb) x/4g car1
0x6000020ff780: 0x00000001001555e8 0x0000000100150040
0x6000020ff790: 0x0000000000000000 0x0000000000000000
(lldb) x/4g car2
0x6000020ff780: 0x00000001001555e8 0x0000000100150040
0x6000020ff790: 0x0000000000000000 0x0000000000000000
(lldb) x/4g car3
0x6000020ff780: 0x00000001001555e8 0x0000000100150040
0x6000020ff790: 0x0000000000000000 0x0000000000000000
(lldb) po 0x0000000100150040
奔驰

sizeof()、class_getInstanceSize()、malloc_size的区别

sizeof():返回的是类型占用内存的大小,那么对象时一个 *指针,指针在内存中占8字节,返回大小自然是8。

class_getInstanceSize():返回的是对象在内存中实际占用的内存大小,根据对象的属性大小计算出来的大小,但是并不是系统分配的内存大小,系统需要将该大小进行内存对齐后才是系统分配的内存大小。

malloc_size():返回系统运行时,实际分配的内存大小。

总结:sizeof返回的就是指针大小,class_getInstanceSize是实际占的内存,也是未进行内存对齐的内存大小,malloc_size才是系统真实分配的内存大小 看到这里在回答面试官的int的初始化过程和init与alloc及new的关系就可以向切菜一样了吧

我也是在重新复习底层知识,如果本文对大家有帮助希望,希望大家收藏点赞。

也欢迎与各位大佬互动探讨,谢谢!