iOS的底层探究--------alloc

448 阅读8分钟

引子

一说到这个alloc,相信最为一个一直面向对象却还是单身的iOS程序员来说,简直再熟悉不过了,其出现的频率,可以说是every day。这样熟悉的代码,也能发现新大陆哦。小板凳摆好,瓜子、啤酒、八宝粥。。。嗯嗯,直接上代码:

   TestPerson *baseP = [TestPerson alloc];
   TestPerson *person1 = [baseP init];
   TestPerson *person2 = [baseP init];
   TestPerson *person3 = [TestPerson alloc];
   
   NSLog(@"%@-------%p-------%p",baseP,baseP,&baseP);
   NSLog(@"%@-------%p-------%p",person1,person1,&person1);
   NSLog(@"%@-------%p-------%p",person2,person2,&person2);
   NSLog(@"%@-------%p-------%p",person3,person3,&person3);

打印得到的结果:

<TestPerson: 0x600001564080>-------0x600001564080-------0x7ffee3e41d28
<TestPerson: 0x600001564080>-------0x600001564080-------0x7ffee3e41d20
<TestPerson: 0x600001564080>-------0x600001564080-------0x7ffee3e41d18
<TestPerson: 0x600001564090>-------0x600001564090-------0x7ffee3e41d10

仔细观察下打印的结果,哟,baseP、person1、person2三者的之间的对象地址竟然是一样的,但是那个指针地址却又不一样。

我们再来看下person3,好家伙,它和baseP、person1、person2不仅对象地址不一样,指针地址也不一样。。。。。。

看到这里,是不是该有点小疑问了😁 产生what is it弄啥嘞的疑问了,(注:此处只有:【有】这个单选,不然小编表示泪奔三千里(^▽^)~ ~ ~ ~ ~ ~ )

从打印结果来看,person3、baseP、person1、person2的指针地址的变化,

  0x7ffee3e41d10 + 0x8 = 0x7ffee3e41d18;
  0x7ffee3e41d18 + 0x8 = 0x7ffee3e41d20; 
  0x7ffee3e41d20 + 0x8 = 0x7ffee3e41d28; 

都是8字节偏移量,而且还是连续的。再有,basePperson1person2的内存地址都是0x600001564080,而person3的内存地址是0x600001564090

所以,使用 alloc 初始化,是要开辟新的内存空间的,而 init 则不能开辟新的内存空间。栈开辟空间是连续的,开辟的内存是由高地址指向低地址

当然了,说到栈,就不由想到堆,那么堆开辟空间,是由低地址指向高地址

看到这里,是不是还是比较朦朦胧胧,这小编到底要说明个啥?嘿嘿,前面是引子,通过地址的变化,可以看出熟悉的alloc好像还有很多我们不熟悉的地方,那么接下来,就我们就一起探寻下alloc中我们不熟悉的区域。(大神们,可以直接飘过了,如有不足之处,相当欢迎斧正哈。。。。)

在开始之前,需要进行一些准备工作

到苹果开源库下载源码,源码地址 9EB3111E-3CA0-4378-9AAD-0E614CE47E40.png (下载后的源码是不能直接运行的,需要进行相应的配置,这里,小编就直接提供一个github里面一个配置好的、很不错的Demo,里面还有详细的配置步骤----GitHub

使用三种探寻底层的方法

当我们自己写一些demo,想去查看alloc的具体操作的时候,苹果只是提供了相应的API,但是没有具体的实现可以用来研究。哼哼,以为这样就能让我们屈服了吗?

31623037855_.pic.jpg

这里,小编分享 三种 探寻alloc的实现流程方法 (^▽^),(低调低调)

方法一、通过符号断点跟踪流程

13882AA8-FF16-402C-BCB5-C5FFC986422E.png 看图操作哈,运行代码,到断点处时,就可以按住 control 键,再点击 step into 开始单步执行。单步执行到对应的底层符号断点处。

在这里就单步执行两步,就进入到objc_alloc方法里面 0476DD39-470A-4F42-903D-C9EF48803972.png

那么我们拿到了objc_alloc方法,就可以用符号断点进行调试 AA59AD28-D128-4D56-BA91-036EA2926DC7.png

我们可以去掉最开始时,在TestPerson*baseP = [TestPerson alloc]; 这行代码处打的断点了。直接使用我们的符号断,运行工程。 C76CF12A-3B76-4068-8A55-78EA4F9E96CC.png

运行后,断点就直接来到我们objc_alloc方法上面来了。我们还可以在汇编当中,看到紧接着还会进入到_objc_rootAllocWithZone方法,然后再是objc_msgSend

根据这个方法,通过符号断点,去断_objc_rootAllocWithZone方法,看接着又调用了哪些方法,以此类推,我们就可以调试到我们需要调试的地方。 (啊哈,风停了、雨歇了,我感觉自己又行了😁)

方法二、通过汇编进行断点调试跟踪

6FA9D7C0-07FD-4FAC-8CA1-C0E6EE6FD8E4.png 我们还是要在TestPerson *baseP = [TestPerson alloc];设置断点。那么,这个时候,debug得设置成汇编模式。

设置步骤:Xcode -> Debug -> Debug Workflow -> Always Show Disassembly FC1CE08D11EDC78E8415744B0814E3C8.png 设置好了之后,我们就开始运行工程了。

通过汇编,当前断点在第7行,我们在第九行设置断点,断住objc_alloc符号。 2A767E95-1990-4933-96BE-C19F9F332C4C.png

当执行到第九行断点时,通过step into进入到objc_alloc方法里面。 6B1CC17D-D4B8-4DF4-B68E-0C48787FDDD2.png

当然,这里也可以使用:按住 control 键,再点击 step into 开始单步执行,这样的方法。

方法三、直接添加想要搜索的符号断点跟踪

就比如,现在要断alloc方法,那么就直接下符号断点(简单、cu bao) 3BAAC074-9057-4C5D-BB4E-CF4356324483.png

哦豁,这个厉害,一步到位,但是。。。。。。嘿嘿 这种方式,只有在你需要断住的地方,再把断点激活,可以在需要断点的前一行再下个断点。等运行到的时候,再设置符号断点。

如果是全程都是在激活状态的话,想想工程里面得有多少个alloc调用,想想就爽。(借你一把锤子,合金的😁)

看到这里,恭喜读者,喜提三种调试方法。但是,又见但是。。。。。这三种方法,虽然有效,但是效率太低了,每次都得不断的进行断点跟踪,不仅繁琐,还还很麻烦。调试的时候,一个不小心,手抖了下,多点击几次,跳过了某个方法,不是又得重来,说起来,就一把泪啊,总得想个比较稳点的方法。嗯嗯。去厕所思考下。。。。。。

有了,如果我们把苹果提供给我们的 objc 源码编译成工程运行起来,嘿嘿嘿,那还不是想怎么样,就怎么样,O(∩_∩)O哈哈~

通过源码编译,进行alloc的源码探究

根据前文中提到的可以编译的源码下载下来,运行后,来跟踪alloc的具体流程。

首先下断点,进入到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__ //判断是不是 objc2.0版本
    //slowpath 大概率为假
    //fastpath 大概率为真
    //其实将fastpath和slowpath去掉是完全不影响任何功能,写上是告诉编译器对代码进行优化,提升编译效率。
    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));
}

紧接着,再查看_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) {
        //将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);
}

根据我们梳理的流程,可以得到一个流程图 4398097-0472d375c63650a6.png

接下来,再对其中的细节再进行分析

首先是计算内存大小的地方
进入instanceSize方法,查看实现
inline size_t instanceSize(size_t extraBytes) const {
        //快速计算内存大小
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }
        //计算类中所有变量需要的内存大小   extraBytes额外字节数一般是0
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        //最小返回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) {
    //16字节对齐算法 &为与操作 ~为取反操作
    return (x + size_t(15)) & ~size_t(15);
}

再深入继续研究下align16这个东西的具体过程,需要有点位运算的小基础哦😁

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 
结果:16

那么我们就能得到: 方法align16,其实际上就是取n * 16 (n为正整数),就比如(m + 15)= sum,m为任意正整数,sum16的整数倍,sum / 16,得到的还是一个整数,余数舍去。这个,就是16字节对齐

那么,为什么要16字节对齐了?

1、提高性能和读取速度。因为CPU在读取数据时,是以字节块为单位进行读取的,如果频繁的读取字节未对齐的数据,降低了CPU的性能和读取速度,所以这样做其实就是用空间换取时间。

2、更安全。之所以是16字节对齐,而不是8字节对齐,因为对象中的isa指针是占8字节的,如果只有8字节的空间单位的话,一个isa指针就占满了,不预留空间,可能造成这个对象的isa指针和另一个对象的isa指针紧挨着,容易造成访问混乱。所以16字节对齐,会预留部分空间,访问更安全

分析完内存计算,就得接着分析calloc,也就是开辟内存,返回地址指针,进入到该方法里面查看实现

通过 instanceSize 方法,计算出所需的内存空间大小,接着就向系统申请 size 大小的内存,赋值给 objc,因此objc是指向内存地址的指针。

来来来,验证下: B8B9E3C1-DB7E-455A-A89C-B2B8076ED092.png

从图中可以看出,当在第一个断点的时候,obj还没有进行赋值,此时有地址值,说明系统给开辟一个占位内存地址; 当执行calloc方法过后,所打印出来的地址(指针地址);

但是和平常见到的地址指针(<TestPerson: 0x7ffee3e41d10>)不一样,为什么呢?

1、因为obj地址还没有和传入的cls进行关联;

2、就是calloc只是开辟内存空间;

接着分析initInstanceIsa方法,初始化指针 ,和类关联起来,查看其实现
进入 initInstanceIsa 方法,查看实现
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类进行关联。

isa指针初始化以后,打印objc的结果是 E1FC362C-2506-4DCD-B4BE-7274390B8E78.png

到了此处,欧耶,大功告成,alloc的底层探究,就完成了,有木有点收获啊,(不许没有啊<( ̄▽ ̄)/)

其实,alloc的主要目的就是开辟内存,并使得isa指针和cls类进行关联。

感谢各位的光临~ ~ ~ ~ ~ ~ 71623057628_.pic.jpg