OC对象占用内存原理 (一文彻底搞懂)

6,080 阅读5分钟

要想真真切切看到一个OC对象占用多少内存, 实践是必不可少的.

初始OC对象占用内存

创建一个 Command Line Tool 工程 , 打开 main.mmain 函数创建一个 NSObject.

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
    }
    return 0;
}

打开终端/iTerm2 , 进入到 main.m 目录. 将其转换为 c++ 源码.

clang -rewrite-objc main.m -o main.cpp

文件夹目录里多出一个 main.cpp 文件 , 打开. 看到98242行代码,不要慌.我们只需要关注 NSObject 即可. 搜索 NSObject_IMPL.

struct NSObject_IMPL {
    Class isa;
};

这个就是 NSOject 对象对应的 C++ 结构体. 里面包含了一个 Class 指针. 搜索发现

typedef struct objc_class *Class;

其实就是一个指向 struct objc_class 结构体类型的指针. 那么也就是说目前我们只发现 NSObject 对象对应的结构体只包含一个 isa 指针变量 , 一个指针变量在 64 位的机器上大小是 8 个字节.

那是不是说一个 NSObject 对象就占用8个字节大小的内存呢?实际上不是的. 答案其实是: 所有的OC对象至少为16字节.

我们先来验证一下. (有兴趣的可以去看看刚刚 main.cpp 中最下面 main 函数中 对象的创建源码)

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *lbobjc = [[NSObject alloc] init];
        
        NSLog(@"lbobjc对象实际需要的内存大小: %zd",class_getInstanceSize([lbobjc class]));
        NSLog(@"lbobjc对象实际分配的内存大小: %zd",malloc_size((__bridge const void *)(lbobjc)));
    }
    return 0;
}

打印结果

iOS-OC对象占用内存探索[2903:181348] lbobjc对象实际需要的内存大小: 8

iOS-OC对象占用内存探索[2903:181348] lbobjc对象实际分配的内存大小: 16

先别着急猜. 我们来看下内存. 打印语句加个断点. 走你.

lldb -> po lbobjc

查看内存具体内容方法:

  • 1️⃣ 打开内存查看工具:

地址栏中输入对象地址: 0x1007579c0

  • 2️⃣ lldb

两种方法都表明, 目前我们创建的对象 后面几个字节全部为 00 .

我们可以通过阅读 objc4 的源码来找到答案。通过查看跟踪 obj4allocallocWithZone 两个函数的实现,会发现这个连个函数都会调用一个 instanceSize 的函数:

size_t instanceSize(size_t extraBytes) {
     size_t size = alignedInstanceSize() + extraBytes;
      // CF requires all objects be at least 16bytes.
      if (size < 16) size = 16;
      return size; 
}

上面源码中我们看出了答案, 最少会开辟16个字节. 那么为什么非要用 16 个字节来存储 8 个字节的内容呢? 这里简单解释一下 .

其实这里主要是涉及到硬件问题, 因为不同厂商之间需要一套标准化方案来解决不同厂商之间规则不同导致内存读取使用出现不统一的情况.为了解决这种问题而产生的 字节对齐.

讲到这里,我还想继续看下 当这个对象包含多个属性时使用内存情况. 以便我们彻底搞明白 OC 对象使用内存情况.

包含其他属性占用内存情况

创建一个 LBPerson 类,继承与 NSObject , 其包含三个 int 属性

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LBPerson : NSObject
@property (nonatomic,assign) int age;
@property (nonatomic,assign) int height;
@property (nonatomic,assign) int row;
@end

回到 main 函数

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LBPerson * obj = [[LBPerson alloc] init];
        obj.age = 4;
        obj.height = 5;
        obj.row = 6;
        NSLog(@"lbobjc对象实际需要的内存大小: %zd",class_getInstanceSize([obj class]));
        NSLog(@"lbobjc对象实际分配的内存大小: %zd",malloc_size((__bridge const void *)(obj)));
    }
    return 0;
}

打印结果

iOS-OC对象占用内存探索[3012:201559] lbobjc对象实际需要的内存大小: 24

iOS-OC对象占用内存探索[3012:201559] lbobjc对象实际分配的内存大小: 32

lldb查看内存

这里就出现一个比较奇怪的现象 , 实际需要内存大小 24 , 为什么呢 ? 其实这里就是 结构体内存分配的原理 了.

  • 结构体每个成员相对于结构体首地址的偏移量都是这个成员大小的整数倍,如果有需要,编译器会在成员之间加上填充字节

  • 结构体的总大小为结构体最宽成员大小的整数倍。

  • 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。

  • 对于结构体成员属性中包含结构体变量的复合型结构体,在确定最宽基本类型成员时,应当包括复合类型成员的子成员。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。

由于原本结构体 isa 指针占用8个 , age 属性占用4个, height 占用 4个, row 属性再占用4个 , 这中间由于满足整除并没有自动偏移补充. 而由于 : 结构体的总大小为结构体最宽成员大小的整数倍 , 而且对线开辟满足 16 字节对齐原则 ( 可以在 libmaclloc 源码查找到 ) , 因此实际总占用内存为24. 而实际开辟则满足对齐标准开辟为 32.

下图为 libmaclloc 源码 , nano_malloc.c

继续将 部分 int 类型修改为 double. 你会发现新的内容,篇幅原因不再讲述. 直接上结果

  • age: int , height : double , row :int

    重点图

  • age: int , height : Double , row :Double

总结 (只考虑64位):

  • OC对象 最少占用 16 个字节内存 .
  • 当对象中包含属性, 会按属性占用内存开辟空间. 在结构体内存分配原则下自动偏移和补齐 .
  • 对象最终满足 16 字节对齐标准 .
  • 属性最终满足 8 字节对齐标准 .
  • 可以通过 #pragma pack() 自定义对齐方式 .