ios 底层原理 01-alloc 底层分析

334 阅读8分钟

前言

我们在开发中最常用的就是 allocinit。先看下面的代码跟运行的结果,会发现p1,p2,p3打印的内存地址是一样的。p1.p2.p3 输出 所以,可以得到下面的结论: 1.alloc让对象有了内存空间跟指针指向。 2.init之后内存并没有发生变化,说明 init 并没有操作指针。而且变量地址是相差 8 字节,说明了栈内存是连续的并且指针占 8 字节内存空间(栈区内存从高地址到低地址,堆区从低地址到高地址).

定位源码的三种方法

1.断点调试

在 alloc 上加一个断点,按住 control+step into 单步往下走一步。发现 objc_alloc方法,添加 objc_alloc 符号断点。通过符号断点发现,alloc 在 libobjc.A.dylib 这个库中。

2.汇编调试

在 xcode 中 debug->debug workflow ->always show disassembly 中打开允许汇编,在 alloc 上在打上断点,会发现进入断点 进入了objc_alloc方法,然后在根据符号断点去查看该方法使用的动态库。

3.通过已知方法进行符号断点

直接添加 alloc enable symbolic breakpoint

结合源码还有汇编调试分析

打开下载的源码 搜索 alloc { :发现 alloc 里面调用的是_objc_rootAlloc 方法。在点击 _objc_rootAlloc,调用的是 callAlloc 调用 callAlloc之后 ,是执行_objc_rootAllocWithZone 还是objc_msgSend呢?

_objc_rootAllocWithZone

先看下 _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

在查看_class_createInstanceFromZone发现有三个重要方法
1.计算需要开辟的内存空间大小instanceSize
2. 申请内存空间calloc
3.将 cls 类和 isa关联initInstanceIsa

instanceSize

在继续查看instanceSize内部的代码
进入到这个函数,首先判断是否有缓存,如果有执行cache.fastInstanceSize函数直接返回,内存开辟结束,获得该对象内存大小。如果没有缓存,会执行alignedInstanceSize函数,执行word_align函数,此函数的参数是函数unalignedInstanceSize,而这个函数通过data()->ro()->instanceSize获取到对象的实例大小,也就是说,最终开辟内存空间的大小是根据对象的成员变量大小决定的。这里我们看一下unalignedInstanceSize()的返回值是多少,跟进去我们发现,该返回值的大小由实例变量的大小决定,依赖于成员变量(ivars): 默认情况下,不创建任何成员变量,类开辟的内存空间是8字节,因为继承NSObject造成的,NSObject内有成员变量isa,由于isa的类型是结构体指针,所以isa是8字节,所以创建一个新的对象,没有任何成员变量,默认内存大小是8字节
PS: instanceSize疑问 走缓存还是下面的分支?

  // May be unaligned depending on class's ivars.
    uint32_t unalignedInstanceStart() const {
        ASSERT(isRealized());
        return data()->ro()->instanceStart;
    }

    // Class's instance start rounded up to a pointer-size boundary.
    // This is used for ARC layout bitmaps.
    uint32_t alignedInstanceStart() const {
        return word_align(unalignedInstanceStart());
    }

    // May be unaligned depending on class's ivars.
    uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

    inline size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        //16字节对齐
            return cache.fastInstanceSize(extraBytes);
        }
       //8字节对齐,至少是16 字节
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;  
        return size;
    }
 

fastInstanceSize

在继续查看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

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

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字节对齐 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


8字节对齐

算法讲解:
首先我们知道此时:x = 8 && WORD_MASK = 7
那么函数中的计算公式就是:(8 + 7) & ~7 也就是 15 & ~7。
15的二进制是 ---> 0000 1111
7的二进制是 ---> 0000 0111,那么~7为 ---> 1111 1000
那么15 & ~7 就是 0000 1111 & 1111 1000,结果为0000 1000 == 8

calloc

calloc分析:申请内存,返回地址指针 通过instanceSize方法计算的内存大小,向内存中申请大小为size的内存,并赋值给obj,因此 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;
    }

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类进行关联

总结

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

跳转到的地方

在类的实现过程中,类是否为非懒加载,如果是非懒加载就会在main函数之前;如果懒加载,就会在第一次发送消息的时候,会对类进行初始化,也就是实现类!过程中会将fastInstanceSize设置到缓存中。

alloc的流程图

补充一下[NSObject alloc]流程,因为在调用NSObject的alloc方法时,alloc已经放入缓存,(系统初始化时,已经被其他的类调用,放入了缓存!)。所以NSObjec alloc流程会直接调用_objc_rootAllocWithZone

init 跟 new

init

    return _objc_rootInit(self);
}

断点进入 _objc_rootInit

_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}
  • init方法返回的是对象本身
  • init可以提供给开发者更多的自由去自定义 ,通过id实现强转,返回我们需要的类型
    new
    return [callAlloc(self, false/*checkNil*/) init];
}

源码显示 走了callAlloc的方法流程,然后走了init方法 ,所以 new看做是alloc + init

总结

alloc 的核心作用就是开辟内存,通过isa指针与类进行关联,init方法,提供开发者更多的自由,new 是对(alloc+init)进行了封装,无法在初始化的时候添加其它的需求。

补充说明

这里面有 slowpathfastpath fastpathfastpath(!cls->ISA()->hasCustomAWZ())

bool hasCustomAWZ() const {
        return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
    }

继续查看FAST_CACHE_HAS_DEFAULT_AWZ

// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define FAST_CACHE_HAS_DEFAULT_AWZ    (1<<14)

也就是说cls->ISA()->hasCustomAWZ()是用来获取类或父类中,是否有alloc/allocWithZone:的实现。再次查看fastpath

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

fastpath&slowpath是两个objc源码中定义的宏。 调用的都是__builtin_expect

__builtin_expect(bool exp, probability)的主要作用是进行条件分支预测。 函数主要有两个参数:
第一个参数:是一个布尔表达式
第二个参数:表明第一个参数为真值的概率,这个参数只能是1或0;当取值为1时,表示布尔表达式大部分情况下的值为真值;当取值为0时,表示布尔表达式大部分情况下的值是假值。 函数的返回值,就是第一个参数的表达式的值。
在一条指令执行时,由于流水线的作用,CPU可以完成下一条指令的取值,这样可以提高CPU的利用率。在执行一条条分支指令时,CPU也会预取下一条执行,但是如果条件分支跳转到其他指令,那么CPU预取的下一条指令就没用了,这样就降低了流水线的效率。__builtin_expect函数可以优化程序编译后的指令序列,使指令尽可能的顺序执行,从而提高CPU预取指令的正确率

例如:
if (__builtin_expect (x, 0))
    foo();
表示:x的值大部分情况下可能为假,因此foo()函数得到执行的机会比较少。这样编译器在编译这段代码的时候,就不会将foo()函数的汇编指令紧挨着if条件跳转指令。
再比如:
if (__builtin_expect (x, 1))
    foo();
表示:x的值大部分情况下可能为真,因此foo()函数得到执行的机会比较大。这样编译器在编译这段代码的时候,就会将foo()函数的汇编指令紧挨着if条件跳转指令。
为了简化函数使用,iOS系统使用两个宏fastpath和slowpath来实现这种分支优化判断处理:
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

那也就是说fastpath(!cls->ISA()->hasCustomAWZ())的结果,其实就是!cls->ISA()->hasCustomAWZ()的结果。
这里还有一个判断__OBJC2__是用来判断是否有编译优化

内存对齐参考链接