iOS 通过源码看看alloc以及内存分配

首先从源码(基于779.1)调试追踪一下alloc的流程。

image.png

大致如上图所示
接下来从一道经典面试题开个头
一个NSObject对象占用多少内存
答:系统分配16字节,实际利用8字节

  • NSObject 只有一个成员变量 isa指针 ,arm64架构后 可以追寻到一个isa_t类型的联合体(union)结构
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

绝大多数情况下,苹果采用了优化的isa策略,即isa_t类型不是class 而是struct

image.png

根据它的位域来看 占用64位,即8字节。各位的含义这里就先不说了
在64位里指针占8字节,看起来一个NSObject对象 8字节 就行了,而不是16字节,这就需要继续探索了。

回到上面的alloc流程图

我们主要来看看·cls->instanceSize(计算开辟内存大小) 和(id)calloc(1, size) (申请开辟内存,返回地址指针)

先看看cls->instanceSize 相关的源码
size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            ///都走快速计算 ,在runtime入口函数里,`map_images`内的`realizeClassWithoutSwift` 的`setInstanceSize` 做cache的setFastInstanceSize
            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;
    }
 size_t fastInstanceSize(size_t extra) const
    {
       ASSERT(hasFastInstanceSize(extra));
            ///__builtin_constant_p(x) : 如果x的值在编译时能确定,那么该函数返回值为1.
       if (__builtin_constant_p(extra) && extra == 0) {
            
            return _flags & FAST_CACHE_ALLOC_MASK16;
       } else {
                /// 即得到`setFastInstanceSize`里 (word_align(newSize) + FAST_CACHE_ALLOC_DELTA16)
            size_t size = _flags & FAST_CACHE_ALLOC_MASK;
            // remove the FAST_CACHE_ALLOC_DELTA16 that was added
            // by setFastInstanceSize
            /// 减去 FAST_CACHE_ALLOC_DELTA16 得到 word_align(newSize),再进行16字节对齐
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }
    ///16字节对齐的算法
static inline size_t align16(size_t x) {
   return (x + size_t(15)) & ~size_t(15);
}

 void setFastInstanceSize(size_t newSize)
    {
        // Set during realization or construction only. No locking needed.
        uint16_t newBits = _flags & ~FAST_CACHE_ALLOC_MASK;
        uint16_t sizeBits;

        // Adding FAST_CACHE_ALLOC_DELTA16 allows for FAST_CACHE_ALLOC_MASK16
        // to yield the proper 16byte aligned allocation size with a single mask
        sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16;
        sizeBits &= FAST_CACHE_ALLOC_MASK;  
        
        if (newSize <= sizeBits) {
            newBits |= sizeBits;
        }
        //uint16_t a = ((_flags & ~FAST_CACHE_ALLOC_MASK)|(((word_align(newSize) + FAST_CACHE_ALLOC_DELTA16))&FAST_CACHE_ALLOC_MASK))&FAST_CACHE_ALLOC_MASK;  
        _flags = newBits;
    }

通过debug发现 ,instanceSize只会走fastInstanceSize, 而这个我看了一下,在runtime入口函数里,map_images内的realizeClassWithoutSwiftsetInstanceSize 做了cachesetFastInstanceSize

所以我们接着往下看fastInstanceSizeelse 判断
  • size_t size = _flags & FAST_CACHE_ALLOC_MASK; 相当于获取到得到setFastInstanceSize(word_align(newSize) + FAST_CACHE_ALLOC_DELTA16),可以在setFastInstanceSize_flags & FAST_CACHE_ALLOC_MASK 验证一下。
  • align16(size + extra - FAST_CACHE_ALLOC_DELTA16) 参数减去了FAST_CACHE_ALLOC_DELTA16,因为在setFastInstanceSize 加了setFastInstanceSize, 这样得到的就是word_align(newSize)
那么看一下word_align
#   define WORD_MASK 7UL   ( 0000 0000 0000 0111)
static inline size_t word_align(size_t x) {  
    return (x + WORD_MASK) & ~WORD_MASK;
}

~WORD_MASK:1111 1111 1111 1000
(x + WORD_MASK) & ~WORD_MASK即把(x+7) 低三位清0,结果将是8的倍数,与16字节对齐算法一样。
那么

#define FAST_CACHE_ALLOC_DELTA16      0x0008
sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16;

FAST_CACHE_ALLOC_DELTA16等于8, 因此得到的sizeBits=8的倍数+8,至少也是16。

  • 所以 size_t size = _flags & FAST_CACHE_ALLOC_MASK;获取到的size>=16且是8的倍数,再通过align16(size + extra - FAST_CACHE_ALLOC_DELTA16); 进行16字节对齐
  • (x + size_t(15)) & ~size_t(15); 这里的xword_align(newSize)
    下面通俗的理解一下
15取反  1111 1111 1111 0000 = 1*2^15 +... 1*2^4 
与15取反 相与 前四位为0 即抹掉前4位  只要是大于15的数与它相与必定是16的倍数
x 默认 + 15  ,如果x小于16 那么+15也是大于16了,自然也是返回16
举个例子:
x = 8
(x + size_t(15)) & ~size_t(15)
15 二进制 :0000 0000 0000 1111
~size_t(15) : 15取反  1111 1111 1111 0000 
x + size_t(15) = 8+15 = 23  0000 0000 0001 0111
23&~15 : 0000 0000 0001 0000 =1*2^4 = 16
所以cls->instanceSize得到的size是16的倍数
接下来看看具体开辟内存空间的方法calloc

这部分源码需要去libmalloc里面看了,可以在mallco.c里找到实现

void *
calloc(size_t num_items, size_t size)
{
    return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}

MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
        malloc_zone_options_t mzo)
{
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

    void *ptr;
    if (malloc_check_start) {
        internal_check();
    }

    ptr = zone->calloc(zone, num_items, size);

    if (os_unlikely(malloc_logger)) {
        malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
                (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
    }

    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
    if (os_unlikely(ptr == NULL)) {
        malloc_set_errno_fast(mzo, ENOMEM);
    }
    return ptr;
}

到这里ptr = zone->calloc(zone, num_items, size);,单看源码无法继续往下了,因此编译源码来debug

  • main函数calloc一下 void *p = calloc(1, 8);
  • ptr = zone->calloc(zone, num_items, size);打上断点,lldb输入s指令下一步,发现进入default_zone_calloc
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    zone = runtime_default_zone();
    
    return zone->calloc(zone, num_items, size);
}
  • 又无法跳转,还是断住,s,发现进入nano_calloc
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
    size_t total_bytes;

    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
        return NULL;
    }
    printf(&total_bytes);
    if (total_bytes <= NANO_MAX_SIZE) {
        void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
        if (p) {
            printf("1111\n");
            return p;
        } else {
            /* FALLTHROUGH to helper zone */
            
        }
    }
    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
    return zone->calloc(zone, 1, total_bytes);
}
  • 调试继续往下看,进入了 void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
    调试看到这里的total_bytes 就是传进来的size (通过这一步calloc_get_size(num_items, size, 0, &total_bytes)操作)
  • 而最终_nano_malloc_check_clear 里面有一个重要方法 size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key);
    通过调试发现slot_bytes 即是分配的大小
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{ /// 部分源码
    MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
    printf(&size);
    void *ptr;
    size_t slot_key;
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
    mag_index_t mag_index = nano_mag_index(nanozone);

    nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);

    ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
}

而最终的segregated_size_to_fit则又是做了一次16字节对齐

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;

    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

可以看到当size=0的时候,size=NANO_REGIME_QUANTA_SIZE ,如下宏,1左移4位 即 0000 0000 0001 0000 即16。

#define SHIFT_NANO_QUANTUM      4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16

size!=0是进行下面的操作

k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; 
slot_bytes = k << SHIFT_NANO_QUANTUM;  

相当于k = (size+16-1) >> 4; slot_bytes = k <<4; 相当于低四位清0, 和上面align16效果一样,得到的将是16的倍数。

例子:
size=8
k = (8+16-1) >>4  即23>>4  
23: 0000 0000 0001 0111 
右移4位 :0000 0000 0000 0001
再左移4位:0000 0000 0001 0000
即低四位清0  得16
由此可见,目前版本中instanceSizecalloc都做了内存的16字节对齐的保证。

最后实践来看看sizeof , class_getInstanceSize , malloc_size 区别

struct NSObject_IMPL {
    Class isa;
};

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 8字节
    int _age; // 4字节

};// 16字节
/// 结构体内存对齐 大小必须是最大成员大小的倍数

struct Student_IMPL {
    struct Person_IMPL Person_IVARS; // 16 字节
    int _no; // 4字节
};// 16字节 
@interface Person : NSObject
{
    @public
    int _age; // 4字节
}
@end
@implementation Person

@end
@interface Student : Person
{
    @public
    int _no;// 4字节
}
@end
@implementation Student

@end

如上Person和Student及其结构体本质
先各自打印出结果

        NSObject *obj = [[NSObject alloc]init];
        NSLog(@"sizeof obj: %zd",sizeof(struct NSObject_IMPL));
        NSLog(@"class_getInstanceSize obj: %zd",class_getInstanceSize([NSObject class]));
        NSLog(@"malloc_size obj: %zd",malloc_size((__bridge const void *)(obj)));
        
        Person *person = [[Person alloc]init];
        person->_age = 4;
        Student *student = [[Student alloc]init];
        student->_age = 3;
        student->_no = 5;
        NSLog(@"sizeof person: %zd",sizeof(struct Person_IMPL));
        NSLog(@"class_getInstanceSize person: %zd",class_getInstanceSize([Person class]));
        NSLog(@"malloc_size person: %zd",malloc_size((__bridge const void *)(person)));
        
        NSLog(@"sizeof student struct: %zd",sizeof(struct Student_IMPL));
        NSLog(@"sizeof student: %zd",sizeof(student));
        NSLog(@"class_getInstanceSize studetn: %zd",class_getInstanceSize([Student class]));
        NSLog(@"malloc_size studetn: %zd",malloc_size((__bridge const void *)(student)));
image.png
1. sizeof
  • 是一个运算符,传进来的是类型,用来计算这个类型占多大内存,这个在 编译器编译阶段 就会确定大小。
    比如sizeof(student),student即指针类型,占8个字节,不管你是传student或者person都是8
    上面结果有一个sizeof(struct Student_IMPL)为24 , 按理Student_IMPL结构体内应该是16+4=20,这是因为结构体内存对齐,大小必须是最大成员大小的倍数,即isa占用的8的倍数。
2. class_getInstanceSize
 size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
 // Class's ivar size rounded up to a pointer-size boundary.
 uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

看注释是 成员变量的大小word_align 是8字节对齐的 , 所以class_getInstanceSize计算出来的大小是8字节对齐的。

3. malloc_size
  • 计算对象实际分配的内存大小 即上面分析的 segregated_size_to_fit,是16字节对齐的。
    追踪malloc_size的源码 最终追踪到
static MALLOC_INLINE size_t
__nano_vet_and_size_inner(nanozone_t *nanozone, const void *ptr, boolean_t inner)
{
    // Extracts the size of the block in bytes. Checks for a plausible ptr.
    nano_blk_addr_t p; // the compiler holds this in a register
    nano_meta_admin_t pMeta;

    p.addr = (uint64_t)ptr; // Begin the dissection of ptr

    if (NANOZONE_SIGNATURE != p.fields.nano_signature) {
        return 0;
    }

    if (nano_common_max_magazines <= p.fields.nano_mag_index) {
        return 0;
    }

    if (!inner && p.fields.nano_offset & NANO_QUANTA_MASK) { // stray low-order bits?
        return 0;
    }

    pMeta = &(nanozone->meta_data[p.fields.nano_mag_index][p.fields.nano_slot]);
    if ((void *)(pMeta->slot_bump_addr) <= ptr) {
        return 0; // Beyond what's ever been allocated!
    }
    if (!inner && ((p.fields.nano_offset % pMeta->slot_bytes) != 0)) {
        return 0; // Not an exact multiple of the block size for this slot
    }
    printf("22222");
    return pMeta->slot_bytes;
}

返回的是pMeta->slot_bytes,而这个pMeta->slot_bytes 在开辟空间时的_nano_malloc_check_clear里 可以看到蛛丝马迹。

ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
最终走到segregated_band_grow 里面有一段赋值
pMeta->slot_bytes = (unsigned int)slot_bytes;

所以malloc_size 即是经过segregated_size_to_fit进行16字节对齐的slot_bytes

如有错误,请大佬纠正。

end

你可能感兴趣的:(iOS 通过源码看看alloc以及内存分配)