OC底层原理01- alloc底层探索

531 阅读5分钟

前言

OC作为一门万物皆对象的语言,那么对象的创建、开辟内存空间究竟是怎样做到的?我们平时创建对象用到alloc alloc底层到底怎样玩的,我们今天就探索一下alloc底层流程

一、alloc对象的指针地址和内存地址

我们先来玩个demo看一下

Xnip2021-06-06_10-38-57.jpg

探究分析得出结论

  • 打印得出 p1p2p3 得到的堆内存地址(对象的指针地址)是一样的都是0x600002a048e0 这3个对象指向同一块内存区域 得到的栈内存地址(对象的指针指向的内存地址)是不同的 内存连续开辟 每个相隔8字节

  • alloc居右开辟内存的功能,而init没有开辟内存的功能

  • p4p1p2p3 打印的对象的指针地址和对象的指针指向的内存地址是不一样的

  • 堆的内存地址是由低地址 到 高地址 如上图p1p2p3堆内存地址是:0x600002a048e0p4的堆内存地址是:0x600002a048f0

  • 栈的内存地址是由高地址 到 低地址 如上图p1p2p3栈内存地址是:0x7ffee52060780x7ffee52060700x7ffee5206068p4的栈内存地址是:0x7ffee5206060

二、底层探索的三种方法

准备工作

1.alloc的源码,需要去苹果的SourceBrowser下载最新版本

2.编译问题解决可以参考源码编译调试

1.符号断点

在需要调试的位置打上断点,当进入断点的时候按住control + step into 进入下一步 然后看到objc_alloc,然后加一个系统符号断点 具体流程如下图

Xnip2021-06-05_17-35-04.jpg

Xnip2021-06-05_17-33-25.jpg

Xnip2021-06-05_16-38-35.jpg

Xnip2021-06-05_16-35-34.jpg

Xnip2021-06-06_11-21-48.jpg

2.汇编跟中调试 + 符号断点

在需要调试的位置打上断点,设置Xcode->Debug->Debug Workflow-> Always Show Disassembly当进入断点的时候按住control + step into 进入下一步 然后看到objc_alloc,然后加一个系统符号断点(添加系统符号断点步骤跟1.符号断点一样) 具体流程如下图

Xnip2021-06-05_17-42-24.jpg

Xnip2021-06-05_17-44-23.jpg

Xnip2021-06-05_17-44-45.jpg

3.直接下符号断点

想探索哪个方法就直接给这个方法添加符号断点 暴力输出 比如我们探究alloc方法就直接给alloc添加符号断点 如下图

Xnip2021-06-05_17-51-14.jpg

Xnip2021-06-06_11-33-46.jpg

三、汇编结合源码调试分析

上面的三种探索方法我们已经知道了objc_alloc是属于libobjc 那么我们就把源码编译跑起来 更深入的进去探索一下

探索流程如下断点进入alloc

+ (id)alloc {
    return _objc_rootAlloc(self);
}

在进入 _objc_rootAlloc

id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

在进入 callAlloc 到这里到底走哪个方法

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif
    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

通过汇编结合代码判断 是先走 _objc_rootAllocWithZone 然后再走objc_msgSend 如下图所示

Xnip2021-06-05_19-58-30.jpg

在进入 _objc_rootAllocWithZone

id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

在进入 _class_createInstanceFromZone

static ALWAYS_INLINE id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;
    `先计算出需要的内存空间大小`
    size = cls->instanceSize(extraBytes);
    
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        `向系统申请开辟内存,返回地址指针 `
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
         `关联到相应的类 `
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

1.在这里就有疑问了 如何 计算出需要的内存空间大小 进入 size = cls->instanceSize(extraBytes)这里探索究竟 进入instanceSize

    inline size_t instanceSize(size_t extraBytes) const {
        `快速计算内存大小`
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }
        `计算类中所有成员变量需要的内存大小`
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        `不够16字节 返回16字节`
        if (size < 16) size = 16;  
        return size;
    }

在进入 cache.fastInstanceSize

 size_t fastInstanceSize(size_t extra) const
    {
        ASSERT(hasFastInstanceSize(extra));

        if (__builtin_constant_p(extra) && extra == 0) {
            return _flags
            & FAST_CACHE_ALLOC_MASK16;
        } else {
            size_t size = _flags & FAST_CACHE_ALLOC_MASK;
            // remove the FAST_CACHE_ALLOC_DELTA16 that was added
            // by setFastInstanceSize
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }

在进入 align16

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

我们探究一下 align16的具体实现以align16(8)为例子:

(8 + size_t(15)) & ~size_t(15);

(8+15) & ~15

0000 1111= 15

取反得到下面~15

1111 0000= ~15

23&~15

0001 0111= 23

1111 0000= ~15

结果 0001 0000 = 16

align16算法就是向上取整数 16的倍数 不足16倍数的全部抹掉

2.向系统申请开辟内存,返回地址指针 进入 obj = (id)calloc(1, size)这里探索究竟 calloc

首先根据instanceSize方法计算出需要内存的大小,然后传入size返回obj因此obj是只想内存地址的指针 我们来验证一下如图所示

Xnip2021-06-05_21-38-46.jpg

obj没进行赋值的时候分配了一块脏地址0x00000001002f6ec5

objc执行完calloc方法返回内存地址0x00000001013595c0

说明已经开辟了内存 但是为什么和我们平常看到的不一样呢?

obj没有和cls进行关联绑定

3.关联到相应的类 进入 initInstanceIsa这里探索究竟

inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

关联上以后打印obj显示<LGPerson:0x1013595c0>

alloc开辟内存 通过isa指针与类进行关联

四、编译器优化

callAlloc 方法被编译器优化了

Build Settings->Optimization Level -> Fastest,Smallest(最快最小优化) 优化结果如下图所示

Xnip2021-06-05_21-04-55.jpg

五、alloc的主线流程

alloc流程图.png

六、字节对齐

  uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }

对象的内存大小由什么决定? data()->ro()->instanceSize成员变量

1.起初大小为8字节 因为NSObject 里面有成员变量 Class isa

Class-》isa结构体指针 所以是8字节

2.不满16 最少16

3.字节对齐

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

return (x + WORD_MASK) & ~WORD_MASK;

(8+7)&~7 = 15 & ~7

0000 1111

1111 1000 ~7

0000 1000 8

0000 0111 7

取8的整数 8字节对齐

为什么是8的倍数 不是16的倍数 或者32的倍数请看下节

cpu以8字节去读取 以空间换取时间 没有超过8字节的对象

七、字节对齐原理

字节对齐查看的3种方式

第一种方式如下图展示

Xnip2021-06-05_22-58-44.jpg

第二种方式如下图展示

Xnip2021-06-05_23-00-56.jpg

Xnip2021-06-05_23-03-45.jpg

第三种方式查看如下图展示

x/6gx介绍 第一个x:16进制打印 6:打印6个 g: 8个字节 x:16进制变量显示 看下图:

LLDB文档.png

Xnip2021-06-05_23-11-35.jpg

1.当把height换成bool值的时候 如下图展示

Xnip2021-06-05_23-23-02.jpg

2.当把height换成double值的时候 p/f 也可以直接打印double如下图展示

Xnip2021-06-05_23-27-35.jpg