iOS开发之alloc底层探索之旅

472

写在前面

现在的iOS市场环境,今日不同往日了,出去面试,面试官问的问题已经不单单停留在UI层了,都是往底层去考验面试者的能力了。动不动就是左一个底层,又一个底层的问,大部分人都是一问三不知,非常的懵逼!面试往往都是以失败告终,那么今天我们就来好好的探索一下底层。

我们的每一个程序的界面的开始基本上都是从实例化一个对象开始的,都在allocinit。那么在OC的底层到底做了些什么操作呢?今天我们就好好的来探索探索alloc底层的工作流程。

1.抛砖引玉

首先看看下面的代码

JPStudent *s1 = [JPStudent alloc];
JPStudent *s2 = [s1 init];
JPStudent *s3 = [s1 init];

NSLog(@"%@-%p-%p",s1,s1,&s1);
NSLog(@"%@-%p-%p",s2,s2,&s2);
NSLog(@"%@-%p-%p",s3,s3,&s3);

打印结果

<JPStudent: 0x600002d583e0>-0x600002d583e0-0x7ffee6343098
<JPStudent: 0x600002d583e0>-0x600002d583e0-0x7ffee6343090
<JPStudent: 0x600002d583e0>-0x600002d583e0-0x7ffee6343088

从上面打印的内容结果分析来看,s1/s2/s3 对象的地址是一样的,但是指针的地址是不一样的。是因为alloc开辟一块内存,而init只是这个类的构造方法,并没有开辟内存的功能,所以三个对象的地址都指向了同一个内存地址空间。为了方便大家理解,我画了下面👇这张图

2.步入正题

底层调试的三种方式

  1. 断点调试跟踪
  2. 符号断点跟踪调试
  3. 汇编跟踪

2.1断点调试

在实例化对象的地方打上断点,待断点走到这里,然后按住control键+step into 单步执行

 单步调试可以看到,我们的底层是objc_alloc

2.2符号断点

步骤1

设置符号断点 设置符号断点

步骤2

下符号断点 下符号断点

这时候运行程序,会定位到相应的符号断点处

2.3汇编跟踪

单步调试,会走到汇编代码里面

3.开启源码探索之旅

我们想去看看alloc的实现,点进去走到这里就走不下去了,这就尴尬了,想深入的了解下,却无从下手了😂,这该怎么办呢???

这是因为这里的具体实现的方法没有暴露出来,我们可以去苹果的开源网站去看看,找下开源的源码,试试看能不能找到答案?

源码地址在这里👉Apple Open Source,去这里Source Browser看更直接一点,有了源码我们就可以更深入的去底层探索一番了。

 从上面的官网截图,我们可以看到苹果开源了很多的源码,苹果还是可以的嘛!开放了这么多的源码,供苹果的开发人员学习。那我们该找哪一个源码来探索alloc的工作流程呢?

上面👆介绍了三种底层探索调试的方法,我们已经知道了我们要找的是objc_alloc,所以我们在开源的官网界面搜索一下

 对了,就是这个objc4里面

在这里有苹果每次更新的版本,我们应该尽量找最新的源码来探索学习。

我们今天要讲的是818.2的版本,因为我电脑上已经有了这个版本配置编译好的了工程代码,这里就不详细介绍源码配置编译的过程了😝,有空会出一期教程的😁。

那么我们就正式的开始一步一步开始探索alloc吧!

打开源码工程,断点一个一个的跟流程

其中fastpath中的 cls->ISA()->hasCustomAWZ() 进行判断一个类是否有自定义的+allocWithZone实现,这里通过断点调试,是没有自定义的实现,所以会执行到 if 里面的代码,即走到_objc_rootAllocWithZone

通过上面的跟踪调试,得到下面👇alloc的的流程图

alloc流程.png

走到_class_createInstanceFromZone这个方法里面是alloc核心流程了,咱们得好好研究分析一番了


_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);
}

_class_createInstanceFromZone 方法执行流程图

_class_createInstanceFromZone.png

3.1 cls->instanceSize

我们进入到cls->instanceSize方法里面去看看,是一个if 的判断,并且有返回值

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;
    }

再进入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);
        }
    }

fastInstanceSize这个方法里面,是对创建对象,所需要的内存大小进行计算,并做16字节对齐处理,并返回计算结果,最后得到size的大小,并通过  calloc开辟内存大小赋值给obj,因此 obj是指向内存地址的指针。

3.2 calloc

通过断点调试,我们发现断点断在了obj = (id)calloc(1, size)

如上图,在lldbpo命令打印了obj,这时候还是nil,那么再单步调式,继续往下走看看

从上图我们可以看到打印了一个16进制的地址,这就是因为执行了下面👇的代码

 id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }

这也印证上述的说法,calloc是开辟内存大小的,在未执行calloc时,po obj nil或者是输出一个脏地址;执行后,再po obj,就返回了地址。但是我们发现,这和我们平时开发的时候打印的不一样啊????不应该是 <JPStudent: 0x10070e2e0> 这种的打印格式吗???

3.3 initInstanceIsa

因为 obj 的地址没有与 cls 进行关联,所以必须要关联对象

    //关联对象方法
    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);

 具体的流程图如下👇

关联对象.png

关联对象就是初始化出来一个isa指针,并将isa指针指向申请的内存地址,再将指针与cls类进行关联,那么现在再po一下看看。 很显然现在的输出结果就是我们熟悉的打印方式了

4.总结

  1. size = cls->instanceSize(extraBytes)计算所需要的内存大小,使用16字节对齐算法
  2. calloc 申请开辟所需要的内存空间
  3. initInstanceIsa 关联到对象,并最终返回实例对象

最后在附上比较完整的流程图,方便大家理解

alloc底层原理流程图.png

🌹请收藏+关注,评论 + 转发,以免你下次找不到我,哈哈😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹