iOS对象的内存分析

avatar
iOS 开发工程师 @抖音视界有限公司

OC是面向对象的程序开发语言,对象是它的基本元素,了解对象的本质以及内存分配对于开发者来说是很重要的,能够让一些莫名其妙的问题不再神秘;在一些面试的时候也会有人经常问到类似的问题,一个最基本的对象占用多少内存空间,一个特定的对象占用多少空间,我们今天的任务也就是聊一聊这两个问题,把这两个问题解决了,iOS对象的也就基本了解的差不多了

NSObject的内存大小

我们先来看一下一个最基本的NSObject占用多大的内存空间,先看一下下面的代码来打印NSObject对象的大小

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {

    NSObject *obj = [[NSObject alloc] init];
    
    NSLog(@"%zd ", malloc_size((__bridge const void *)(obj)));
    NSLog(@"%zd ", class_getInstanceSize([NSObject class]));
    
    return 0;
}

打印结果

2023-02-01 15:46:32.041969+0800 TestOC[83749:7940065] 16
2023-02-01 15:46:32.043200+0800 TestOC[83749:7940065] 8

从结果来看,大家应该有两个疑问?

  • 为什么两个获取对象大小的函数 malloc_sizeclass_getInstanceSize 获取同一个对象的内存大小不一样呢?
    • malloc_size 返回系统实际分配的内存大小
    • class_getInstanceSize 返回实际需要分配的内存大小
  • 为什么实际需要分配的大小是8,而系统分配大小是16呢?
    • class_getInstanceSize大小为8
      • 涉及到 NSObject的底层结构以及class_getInstanceSize的内部实现
    • malloc_size返回16
      • alloc分配内存大小流程就知道了

class_getInstanceSize大小为8

NSObject的底层结构

从系统文件分析

我们可以看下系统中对NSObject的定义,位于 usr/include/objc/NSObject.h

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

+ (void)load;
+ (void)initialize;
// ...其他方法

@end

从结构上看NSObject只有一个Class isa变量,那Class又是个啥呢?看下面的定义,位于 usr/include/objc/objc.h,它是一个 struct objc_class *类型的指针

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

从生成的源码分析

对上面的代码执行指令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp,在生成的main.cpp中可以看到下面的代码

struct NSObject_IMPL {
    Class isa;
};

typedef struct objc_class *Class;

总结

NSObject 底层是一个结构体,内部只有一个 Class 类型的 isa 变量,Class是一个struct objc_class *类型的指针,也就是说NSObject内部只有一个isa指针变量

class_getInstanceSize 的内部实现

这个方法的内部实现需要下载objc源码 opensource.apple.com/tarballs/ob…

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());
}

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

从实现可以看出,class_getInstanceSize 内部是返回了内存对齐之后的对象大小

关于内存对齐的含义可以自行google,或参考下面的内存对齐,有两条规则:

  • 数据成员对齐规则:structunion的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行
  • 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,structunion本身也要进行对齐,对齐将按照#pragma pack指定的数值和structunion最大数据成员长度中,比较小的那个进行。

iOS 中 #pragma pack 默认是8

iOS的64位系统中,一个指针占用8个字节,根据内存对齐规则,所以 class_getInstanceSize([NSObject class])返回8

malloc_size返回16

malloc_size 获取的是实际分配的内存空间大小,每个对象为何占用这么多内存空间,这是由 alloc 函数来决定的,下面我们就来深入alloc函数内一探究竟

+ (id)alloc {
    return _objc_rootAlloc(self);
}

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

static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
    // ... 其他与内存分配无关代码省略
}

id _objc_rootAllocWithZone(Class cls, objc_zone_t zone __unused) {
    return _class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC);
}

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) {
    // ... 其他与内存分配无关代码省略
    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);
    }
    // ... 其他与内存分配无关代码省略
}

alloc 最终是由 _class_createInstanceFromZone 内调用 instanceSize() 的来计算需要的内存。由实现可知,如果传入的值小于16的话,直接返回16,也就是注释上说的:CF(CoreFoundation)要求所有的对象至少16个字节

inline size_t instanceSize(size_t extraBytes) const {
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        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;
}

也就是说在创建NSObject的时候需要8个字节(Class isa 指针只占8个字节大小),但是由于小于16,所以分配了16个字节,所以 malloc_size返回16

iOS 对象的内存

对于自定义的iOS对象的内存分配,此处以Person为例,有两个成员变量,age、height,代码如下

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

@interface Person : NSObject {
@public
    int age;
    int height;
}
@end

@implementation Person
@end

int main(int argc, const char * argv[]) {
    Person *person = [[Person alloc] init];
    person->age = 20;
    person->height = 10;
    NSLog(@"%zd ", malloc_size((__bridge const void *)(person)));
    NSLog(@"%zd ", class_getInstanceSize([Person class]));
    
    return 0;
}

可以看到结果 malloc_sizeclass_getInstanceSize 的结果都是 16

分析如下:

使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp指令查看一下 Person 的底层结构

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int age;
    int height;
};
// 等同于
struct Person_IMPL {
    Class isa;       // 8字节
    int age;         // 4字节
    int height;      // 4字节
};
// 总共16个字节

按照之前对malloc_sizeclass_getInstanceSize的探索可知结果是16是正确的

查看 person 对象的内存分布

return 0;处断点,在控制台打印 person 的地址,通过 Debug => Debug Workflow => View Memory 查看 person 对象的内存分布;也可以使用 x person 直接打印出内存分布,参考如下 image.png

iOS对于对象的内存分配也有一些优化,参考 juejin.cn/post/684490…

iOS底层内存分配函数 你不知道的细节(可选)

通过上面的分析,在创建对象的时候会调用alloc函数,内部会调用到_class_createInstanceFromZone,它通过instanceSize函数计算需要分配的内存大小,最终调用malloc_zone_calloc函数来分配内存。但实际上malloc_zone_calloc也有它自己的内部实现。

源码下载 github.com/apple-oss-d…

在源码中搜索 malloc_zone_calloc 能得到下面的结果

void * malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size) {
    return _malloc_zone_calloc(zone, num_items, size, MZ_NONE);
}

static void * _malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
        malloc_zone_options_t mzo) {
    if (zone == default_zone && !lite_zone) {
        zone = malloc_zones[0];
    }
    if (os_unlikely(malloc_instrumented || malloc_check_start ||
                malloc_logger || zone->version < 13)) {
        return _malloc_zone_calloc_instrumented_or_legacy(zone, num_items, size, mzo);
    }
    return zone->calloc(zone, num_items, size);
}

分配内存空间涉及到 malloc_zone_t 以及它的 calloczone->calloc(zone, num_items, size);) 函数,并且有 version 字段的判断(zone->version < 13

找到 malloc_zone_t 的定义

typedef struct _malloc_zone_t {
    /* Only zone implementors should depend on the layout of this structure;
    Regular callers should use the access functions below */

    void    *reserved1; /* RESERVED FOR CFAllocator DO NOT USE */
    void    *reserved2; /* RESERVED FOR CFAllocator DO NOT USE */
    size_t  (* MALLOC_ZONE_FN_PTR(size))(struct _malloc_zone_t *zone, const void *ptr); /* returns the size of a block or 0 if not in this zone; must be fast, especially for negative answers */
    void    *(* MALLOC_ZONE_FN_PTR(malloc))(struct _malloc_zone_t *zone, size_t size);
    void    *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
    void    *(* MALLOC_ZONE_FN_PTR(valloc))(struct _malloc_zone_t *zone, size_t size); /* same as malloc, but block returned is set to zero and is guaranteed to be page aligned */
    void    (* MALLOC_ZONE_FN_PTR(free))(struct _malloc_zone_t *zone, void *ptr);
    void    *(* MALLOC_ZONE_FN_PTR(realloc))(struct _malloc_zone_t *zone, void *ptr, size_t size);
    void    (* MALLOC_ZONE_FN_PTR(destroy))(struct _malloc_zone_t *zone); /* zone is destroyed and all memory reclaimed */
    const char  *zone_name;

    // ... 其他可选方法 略
    unsigned version;
} malloc_zone_t;

这里的注释说的很清楚 Only zone implementors should depend on the layout of this structure(只有实现者应该依赖于这个结构的布局)也就是说这相当于一个抽象类,需要其他类去实现它

下面我们搜索 calloc = 或者 version =,(因为可以直接赋值的方式给结构体的变量赋值)最终确认是下面的搜索结果

image.png

nanozone->basic_zone.calloc = OS_RESOLVED_VARIANT_ADDR(nanov2_calloc); 找到对应的函数 nanov2_calloc

MALLOC_NOEXPORT void * nanov2_calloc(nanozonev2_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;
    }
    // 计算需要的大小
    size_t rounded_size = _nano_common_good_size(total_bytes);
    // 分配小对象时 使用 nanozonev2_t 类型的 zone
    if (total_bytes <= NANO_MAX_SIZE) {
        // 具体的分配细节... 略
    }

    // Too big for nano, so delegate to the helper zone.
    // 分配大对象时 使用 create_scalable_szone 创建的 szone_t 类型的zone
    return nanozone->helper_zone->calloc(nanozone->helper_zone, 1, total_bytes);
}

nanov2_calloc 内部是通过 _nano_common_good_size 来计算内存分配的,具体实现如下

#define NANO_MAX_SIZE           256 /* Buckets sized {16, 32, 48, ..., 256} */
#define SHIFT_NANO_QUANTUM      4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16

static MALLOC_INLINE size_t _nano_common_good_size(size_t size) {
    return (size <= NANO_REGIME_QUANTA_SIZE) ? NANO_REGIME_QUANTA_SIZE
        : (((size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM) << SHIFT_NANO_QUANTUM);
}

_nano_common_good_size 函数简单理解就是:根据入参size大小,如果小于16,就返回16,否则返回 16 的倍数。也就是说 _nano_common_good_size 返回的值一定是 16 * n (n>0)

总结:[NSObject alloc] 最终分配的大小由 _nano_common_good_size()函数决定的,且大小必定是 16 的倍数。

这样就好理解下面的一段代码了

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

@interface Person : NSObject {
@public
    int age;
    int height;
    int score;
}
@end

@implementation Person
@end

int main(int argc, const char * argv[]) {
    Person *person = [[Person alloc] init];
    NSLog(@"%zd ", malloc_size((__bridge const void *)(person)));
    NSLog(@"%zd ", class_getInstanceSize([Person class]));
    
    return 0;
}

结果打印

2023-02-02 00:24:45.815252+0800 TestOC[33836:8445818] 32
2023-02-02 00:24:45.815855+0800 TestOC[33836:8445818] 24

结果分析:Person 的底层结构体按照之前的规律,应该是

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int age;
    int height;
    int score;
};
// 等同于
struct Person_IMPL {
    Class isa;       // 8字节
    int age;         // 4字节
    int height;      // 4字节
    int score;       // 4字节
};
// 总共20个字节,对齐之后是24个字节

Person 底层需要24 个字节,但是 alloc 函数最终调用的 _nano_common_good_size()函数返回的是16的倍数,所以计算后返回32