iOS底层原理–alloc流程探究

前言

敲了五年的代码,敲了无数个alloc,但是alloc在我们看不到的底层代码里到底都干了些什么?迄今我都还不知道?‍♀️。所以不妨今天就趁这个机会,来看看alloc究竟在底层干了哪些不为人知的事情。

  • 在开始之前先来看一个之前遇到过的面试题,大致如下:
    YSHStudent *baseStu = [YSHStudent alloc];
    YSHStudent *stu1 = [baseStu init];
    YSHStudent *stu2 = [baseStu init];
    YSHStudent *stu3 = [baseStu init];
    YSHStudent *newStu = [YSHStudent alloc];
    
    NSLog(@"%@---------%p---------%p",baseStu,baseStu,&baseStu);
    NSLog(@"%@---------%p---------%p",stu1,stu1,&stu1);
    NSLog(@"%@---------%p---------%p",stu2,stu2,&stu2);
    NSLog(@"%@---------%p---------%p",stu3,stu3,&stu3);
    NSLog(@"%@---------%p---------%p",newStu,newStu,&newStu);
复制代码

此时打印baseStu、stu1、stu2、stu3、newStu对象地址、指针地址有什么异同?

咱们直接打印看下结果。

2021-06-06 10:57:53.766217+0800 alloc[27048:1427279] <YSHStudent: 0x600001f68770>---------0x600001f68770---------0x7ffeeeb8a500
2021-06-06 10:57:53.766808+0800 alloc[27048:1427279] <YSHStudent: 0x600001f68770>---------0x600001f68770---------0x7ffeeeb8a4f8
2021-06-06 10:57:53.767075+0800 alloc[27048:1427279] <YSHStudent: 0x600001f68770>---------0x600001f68770---------0x7ffeeeb8a4f0
2021-06-06 10:57:53.767250+0800 alloc[27048:1427279] <YSHStudent: 0x600001f68780>---------0x600001f68780---------0x7ffeeeb8a4e8
复制代码

通过打印结果可以直接看到:

baseStu和stu1、stu2、stu3的对象地址是一模模一样样的,但是指针地址却不同;

newStu和baseStu、stu1、stu2、stu3不仅指针地址不同,而且对象地址也是不同的;

并且发现指针地址的一个规律,由下至上0x7ffeeeb8a4e8 + 8 = 0x7ffeeeb8a4f0、0x7ffeeeb8a4f0 + 8 = 0x7ffeeeb8a4f8、0x7ffeeeb8a4f8 + 8 = 0x7ffeeeb8a500,是一串连续的相差8字节的指针地址,由此我们可以推断栈内存是连续的并且指针占8字节内存空间(PS:栈区内存从高地址到低地址;堆区从低地址到高地址)

下面我们进入正题,正式开始探索alloc底层原理。

准备工作

  1. 下载源码,源码地址(苹果开源库)
  2. 下载下来的源码是不能直接编译的,此时为节省时间可以阅读国内某位酷大神博客或者直接下载github上的可编译源码编译源码地址
  3. 其实正常来说我们是不知道alloc是属于哪个开源库的,如何知道alloc属于哪个开源库呢,下面我介绍3种方法。

三种常用探索底层方法

一、下符号断点直接跟流程

  1. 在需要调试的先打上一个断点,然后按住control键+step into,单步往下走一步.

image.png
2. 单步走,发现objc_alloc方法,添加objc_alloc符号断点。
image.png
3. 通过符号断点发现,alloc在libobjc.A.dylib这个库里。
image.png

二、直接添加想要探索的方法的符号断点

例如直接添加alloc符号断点。(PS:在自己用到的地方再激活该符号断点,alloc在项目里不知道会被调用多少次,直接激活,估计键盘都要被自己点报废了)

image.png

三、查看底层汇编看流程

了解过逆向的同学,这点肯定不陌生。可以说是最常用的方法之一。具体打开汇编调试的方法如下图:

image.png
打开后,到alloc断点后,会进入如下页面,然后发现会调用objc_alloc,可以再添加符号断点,然后单步走跟流程。

image.png

以上就是3种底层探索的方法,最后我们都可以拿到alloc是在libobjc.A.dylib这个库里。

编译运行源代码工程

将我们下载的可编译的源码运行起来,然后我们跟下alloc的具体流程。

1. 下断点,进入alloc源码实现

+ (id)alloc {
    return _objc_rootAlloc(self);
}
复制代码

2. 进入_objc_rootAlloc源码实现

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

3. 进入callAlloc源码实现

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__  //是否有可用的编译器优化
//slowpath 大概率为假
//fastpath 大概率为真
//此处去掉slowpath和fastpath对代码逻辑没有丝毫影响,应该只是告诉编译器对代码优化,提升编译效率。
    if (slowpath(checkNil && !cls)) return nil;
    //判断一个类是否有自定义的 +allocWithZone 实现,没有则走到if里面的实现
    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));
}

复制代码

4. 进入_objc_rootAllocWithZone源码实现

id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    //这里没有什么好说的,直接进入第5步
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

复制代码

5. 进入_class_createInstanceFromZone源码实现

进入到这一步就发现,代码比前面几步都要多,应该是进入核心部分了。
通过对源码分析,该方法中有3个核心方法,分别做了3件重要的事情:

  • cls->instanceSize(extraBytes);——-计算内存空间
  • (id)calloc(1, size);——-开辟内存空间,返回内存地址指针
  • obj->initInstanceIsa(cls, hasCxxDtor);——-关联isa和类
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) {
        //将cls类和isa关联
        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);
}

复制代码

对源码的分析,我们还可以得出该方法内部实现流程图如下:

image.png
下面我们对这几个关键方法的实现进行分析

  • 分析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.
        if (size < 16) size = 16;
        return size;
    }
复制代码

进入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
            //16字节对齐
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }
复制代码

进入align16源码实现

static inline size_t align16(size_t x) {
    //16字节对齐算法 &为与操作 ~为取反操作
    return (x + size_t(15)) & ~size_t(15);
}
复制代码

我们发现此方法是16字节对齐算法,在解析该算法前,我们需要了解为什么要进行16字节对齐。

  • 我们需要知道CPU在读取数据时,是以字节快为单位进行读取的,如果频繁读取没有对齐的数据,会严重加大CPU的开销,降低效率
  • 为什么16字节对齐而不是8字节对齐,我们都知道在一个对象中,第一个属性isa占8字节,如果只有8字节的话,不预留空间,可能造成这个对象的isa和另一个对象的isa紧挨着,容易造成访问混乱。同时一个对象也不会只有isa一个属性。

由此可见:16字节对齐后,可以加快CPU读取速度,同时访问也会更加安全。

16字节对齐算法的过程,如下所示

x + size_t(15)) & ~size_t(15)
&为与操作 ~为取反操作
&(与)的规则是:全部为1则为1,反之则0
~(取反)的规则是:1变0,0变1
此处我们以9为例
9+15=24
24的二进制为:0001 1000
15的二进制位:0000 1111
15的取反二进制位:1111 0000
  0001 1000
  1111 0000
= 0001 0000 也就是10进制的16
复制代码
  • calloc分析:申请内存,返回地址指针

通过instanceSize方法计算的内存大小,向内存中申请大小为size的内存,并赋值给obj,因此 obj是指向内存地址的指针。如图所示

image.png
细心的我们发现,打印出来的格式和之前我们打印<YSHStudent 0x00000001000ebe00>这种格式有些不同。

1.主要是因为obj地址还没有和传入的cls进行关联

2.同时恰恰印证calloc的根本作用只是开辟内存空间

  • obj->initInstanceIsa源码解析:类与isa关联
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

复制代码

经过calloc之后,内存已经申请好了,并且类也已经传进来。所以initInstanceIsa主要就是初始化一个isa指针,并将isa指针指向已经申请好的内存地址,再将isa指针与cls类进行关联。
我们单步往下走断点,走完initInstanceIsa方法后,打印obj发现就和之前打印的格式一样了,已经关联起来了。

image.png

最后

根据源码的分析,我们大体能画出alloc在底层实现的流程图。

alloc流程图.png

总结

  • 通过对alloc源码的分析,可以得知alloc的主要目的就是开辟内存,关联isa和cls类。
  • 其中开辟内存的核心步骤有3步:计算内存大小 — 申请内存空间 — 关联isa和cls类
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享