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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容