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 直接打印出内存散布,参考如下

iOS对象的内存分析

iOS关于目标的内存分配也有一些优化,参考 /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 =,(因为能够直接赋值的方式给结构体的变量赋值)终究确认是下面的查找成果

iOS对象的内存分析

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