alloc最熟悉的陌生人

422 阅读4分钟

只怪我们爱得那么汹涌,爱得那么深,于是梦醒了搁浅了沉默了挥手了,却回不了神,如果当初在交会时能忍住了激动的灵魂,也许今夜我不会让自己在思念里沉沦...咳咳,歌唱完了,今天我们来看一下alloc,这个最熟悉的陌生人。

源码分析

在Xcode中,点击alloc只能进到objc/NSObject.h里面看到方法的声明,内部究竟做了什么我们无从得知。不过苹果提供了objc的源码,这不是最爽的,还有更爽的,酷细大神的可编译objc源码,我们可以编译运行,断点调试。

image.png 通过源码,点击进入alloc走到如下流程

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

__OBJC2__是判断是否为objc2.0版本,兼容objc1.0的逻辑,现在都是objc2.0

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(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

但是当我们使用真机调试,断点进入汇编查看,发现并没有走alloc而是objc_alloc,去源码中搜索objc_alloc

image.png

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

objc_alloc这个函数打下断点,发现最终alloc的流程是

image.png 为什么alloc会走到objc_alloc呢,通过源码的查找,我们找到了一个函数fixupMessageRef,这个函数把alloc,allocWithZone,retain,release,autorelease等方法的IMP都改变了,fixupMessageRef在_read_images中被调用,_read_images是在编译时调用的,说明在llvm中,会改变这个方法的imp。 image.png

结构体内存对齐

结构体内存对齐原则

  1. 结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。
  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.成员结构体的大小为本身对齐后的大小。
  3. 结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补⻬。

练习

struct Struct1 {
    double a;       // 8    [0 7]
    char b;         // 1    [8]
    int c;          // 4    (9 10 11 [12 13 14 15]
    short d;        // 2    [16 17] 24
}struct1;

struct Struct2 {
    double a;       // 8    [0 7]
    int b;          // 4    [8 9 10 11]
    char c;         // 1    [12]
    short d;        // 2    (13 [14 15] 16
}struct2;

struct Struct3 {
    double a;    // 8 [0 7]
    int b;         //4 [8 9 10 11]
    char c;      // 1 [12]
    short d;     // 2  (13 [14 15]
    int e;        // 4 [16 17 18 19]
    struct Struct1 str;    // 8 (20 21 22 23 [24 ~ 47] 48
}struct3;
上面计算的Struct1对齐后的大小为24,Struct1作为成员时大小就是24

image.png

OC对象的大小

计算OC对象大小有两个函数,一个是class_getInstanceSize,一个是malloc_size,但是他们有时候对于同一个对象的计算结果是不同的,下面我们看一下他们的区别。

class_getInstanceSize

class_getInstanceSize是runtime的一个方法,可以通过查看objc的源码看内部实现。点击进去,我们会进入到下面的方法

uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

可以看出需要unalignedInstanceSize()计算出一个没对齐的大小,传到给word_align函数做字节对齐,点击unalignedInstanceSize()进去

// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }
计算类中包含的成员变量大小的和

点击进入word_align

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
WORD_MASK是个宏定义,在64位架构中是7,这是一个8字节对齐的算法
假如:x = 18
          18 + 7 = 25 = 0001 1001
7 = 0000 0111     ~ 7 = 1111 1000   
                        0001 1000 = 24

class_getInstanceSize是计算类中成员变量的小大之和,再做8字节对齐,得出的结果是对象在内存中所需要的大小。

malloc_size

malloc_size返回的是对象在堆区中所占的真实内存大小,通过malloc的源码,我们可以探索一下对象是怎样开辟堆空间大小的。

void *p = calloc(1, 20);  //在这里断点
NSLog(@"%lu",malloc_size(p));

image.png 最后看到这样的一个算法

k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; 
slot_bytes = k << SHIFT_NANO_QUANTUM;	
SHIFT_NANO_QUANTUM 4
NANO_REGIME_QUANTA_SIZE  1 << 4 = 16
20 + 16 - 1 = 35 = 0010 0011 
然后 右移4位,再左移4位,相当于把后4位清零
0010 0011 >> 4 = 0000 0010
0000 0010 << 4 = 0010 0000 = 32
这个算法是一个16字节对齐的算法

所以,对象实际在内存中开辟的空间是16字节对齐的。

总结

每一个对象都至少包含一个成员变量isa,占8字节。大部分情况下,我们创建的对象都会添加成员变量。所以,在实际开辟堆区内存的时候,苹果默认做了16字节对齐的处理。这样的设计既不会让对象的空间被压缩的很小,而且还能尽量少的浪费内存(相比32字节对齐等)。在做字节对齐的时候,苹果的两种算法是非常好的,我们可以学习一下这种位运算的算法。

例如:16字节对齐,可以使用两种算法

  1. x + 16 - 1 & ~(16 - 1)
  2. (x + 16 - 1) >> 4 << 4