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_size和class_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,或参考下面的内存对齐,有两条规则:
- 数据成员对齐规则:
struct或union的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行- 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,
struct或union本身也要进行对齐,对齐将按照#pragma pack指定的数值和struct或union最大数据成员长度中,比较小的那个进行。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_size 和 class_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_size 和 class_getInstanceSize的探索可知结果是16是正确的
查看 person 对象的内存分布
在 return 0;处断点,在控制台打印 person 的地址,通过 Debug => Debug Workflow => View Memory 查看 person 对象的内存分布;也可以使用 x person 直接打印出内存分布,参考如下
iOS对于对象的内存分配也有一些优化,参考 juejin.cn/post/684490…
iOS底层内存分配函数 你不知道的细节(可选)
通过上面的分析,在创建对象的时候会调用alloc函数,内部会调用到_class_createInstanceFromZone,它通过instanceSize函数计算需要分配的内存大小,最终调用malloc_zone_calloc函数来分配内存。但实际上malloc_zone_calloc也有它自己的内部实现。
在源码中搜索 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 以及它的 calloc (zone->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 =,(因为可以直接赋值的方式给结构体的变量赋值)最终确认是下面的搜索结果
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