1.2 OC对象内存分配 - 476139183/Learning-iOS GitHub Wiki

OC 对象的内存分配

我们发现OC的对象和C++的结构体很类似。而实际上,我们通过代码转换,是能够得到 OC 转换为C/C++代码的。

Objective-C 是C/C++的严格超集,底层其实都是C/C++代码实现。因为Objective-C的原意就是在C语言主体上加入面向对象的特性。

1. NSOject对象的大小

NSOject对象对应的C++结构体

struct NSObject_IMPL { 
  Class isa; //一个指向struct objc_class结构体类型的指针 
}; 

typedef struct objc_class *Class

通过结构体我们可以发现,一个 NSObject 对象,里面只有一个 isa 指针,那么一个指针在 64位机器上,大小为 8个字节。如果我们转化为结构体的话,那么他只需要 分配 8 个字节就足够了。(参考 结构体的内存对齐

NSObject *objc = [[NSObject alloc] init];

NSLog(@"objc对象实际需要的内存大小: %zd", class_getInstanceSize([objc class]));
NSLog(@"objc对象实际分配的内存大小: %zd", malloc_size((__bridge const void *)(objc)));

输出的结果:

objc对象实际需要的内存大小: 8
objc对象实际占用的内存大小: 16

也就是 我们得到的 NSObject 对象,系统分配了 16个字节。但是这个对象明明只需要 8个字节的

这就需要我们通过断点去查看 allocallocWithZone 的底层函数去寻找答案。(这里有 objc4-750的 源码

通过 追踪 我们发现 在 objc-runtime-new.mm 文件的 _class_createInstanceFromZone 方面里面,有这么一段代码

 size_t size = cls->instanceSize(extraBytes);

断点调试的时候发现这个 size = 16

而 这个 instanceSize 函数的实现,如下

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

也就是说 系统分配内存的时候,最少是分配 16 个字节大小的。

2. 对象的内存分配规则

我们再深入了解一下 对象的内存分配规则,创建一个 Person,里面有一个成员变量 age

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

@implementation Person 
@end

Person类对应的结构体实现

struct Person_IMPL { 
  struct NSObject_IMPL NSObject_IVARS;//实际上就是一个isa指针 
  int age; 
};

struct NSObject_IMPL { 
  Class isa; //一个指向struct objc_class结构体类型的指针
}; 

//简化版本 
struct Person_IMPL  { 
  Class isa; 
  int age; 
};

我们通过转换的结构体,可以知道 Person 有两个成员变量:一个isa指针和一个int变量。占用内存为 8+4=12。

我们验证一下:

Person *person = [[Person alloc] init];
NSLog(@"Person对象实际需要的内存大小: %zd", class_getInstanceSize([Person class]));
NSLog(@"Person对象实际分配的内存大小: %zd", malloc_size((__bridge const void *)(person)));

打印结果:

Person对象实际需要的内存大小: 16
Person对象实际分配的内存大小: 16

我们发现 Person 对象 实际需要的内存是 16 个字节,而不是 12,这说明了 我们的实际分配内存 也是要字节对齐的,而对齐的规则,参考 结构体内存对齐

Person 系统分配的字节大小也是 16,这个毋庸置疑。

如果超过 16个字节,对齐规则是什么呢?

我们给 Person 对象 新增几个成员变量

{
  int age;
  double weight;
}

那么 根据我们的推断,Person 对象 实际需要的内存,根据结构体对齐规则,我们可以得出是 24。那么系统分配的字节是多少呢?我们打印一下

Person对象实际需要的内存大小: 24
Person对象实际分配的内存大小: 32

可以看到,我们实际需要的内存,其实就是根据 结构体规则 去计算 对齐的。也就是 24个字节。而且通过断点调试,我们也发现了 在函数 instanceSize 返回的 字节是 24

点击进去这个对齐方法,里面有一个 word_align 函数。这个函数会让返回的字节是 8 的倍数

但是实际分配内存的的大小却是 32字节。这又是为什么呢?

OC 对象 分配内存规则

我们继续断点走下去,直到这一步,我们发现对象开始创建,size 传递进去了。

所以我们需要去跟进 calloc 方法去探究, 而这个方法 在 libmalloc 源码中 。在该源码中,找到 函数 void * calloc(size_t num_items, size_t size)。后续操作这里暂时不提,最后我们定位到 函数

static MALLOC_INLINE size_t segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)

在函数中,我们依然可以看到如下截图:

查看 函数 segregated_size_to_fit

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

NANO_REGIME_QUANTA_SIZE 等于 16

我们可以发现,返回的 slot_bytes 其实就是 将 24 进行16位对齐。也就是说,我们OC对象初始化,分配的内存都是 16的倍数

验证

我们继续给 Person 添加 几个成员变量

{
  int age;
  double weight;
  double height;
}

输出结果:

Person对象实际需要的内存大小: 32
Person对象实际分配的内存大小: 32

既是 8的倍数 也是 16的倍数。我们再添加一个成员进行验证

{
  int age;
  double weight;
  double height;
  int no
}

输出结果为

Person对象实际需要的内存大小: 40
Person对象实际分配的内存大小: 48

当前 Person 对应的结构体 成员内存(8+4+8+8+4),根据 结构体内存分配规则,我们可以得出 对齐之后 是所需要的内存大小是 40。这个和我们得出的结果一致。 40再进行16倍数对齐,得到是 48。 也就是系统分配的内存大小是 48 字节。和结果一致。

思考

为什么 OC 对象的内存要限制为 16 的倍数呢?

  • 坊间推测理由如下: 任何一个对象都有自己的 isa, 内存已经占了8字节, 如果采用8字节对齐的话, 对象之间 isa 都是紧邻的, 没有一点空隙, 如果内存访问出现了一点错误或者偏移, 就会访问到其他对象, 就会出现一些野指针,内存访问错误之类, 发生了混乱, 所以为了使得对象之间的访问更加的安全, 就需要给对象之前预留一部分的空间, 预留多少合适呢, 毫无疑问, 当然是8字节, 为什么不是1,2,4字节呢? 一方面16字节是可以占用最小空间的合理内存空间, 即8的倍数, 另一方面也更加的安全, 各个对象的内存都是16字节, 偏移和访问时, 可以很好地进行对齐. 

3.编译器优化

结构体优化

我们设计两个OC对象,分别作为父类和子类,观察继承的时候,生成的结构体是否会进行优化

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

@interface Student : Person {
  int no;
}
@end

那么它们转换的结构体 为

struct Person_IMPL { 
  struct NSObject_IMPL NSObject_IVARS;
  int age; 
};

struct Person_IMPL { 
  struct Person_IMPL Person_IVARS;//父类 
  int no; 
};

那么我们根据它们转换的结构体,定义两个新的结构体

struct PersonIMP {
  Class isa;
  int age;
} PersonIMP;

struct StudentIMP {
  struct PersonIMP person;
  int no;
} StudentIMP;

现在我们单纯分析 对象所需要的内存。并且打印出来

 NSLog(@"Person对象实际需要的内存大小: %zd", class_getInstanceSize([Person class]));
 NSLog(@"Student对象实际需要的内存大小: %zd", class_getInstanceSize([Student class]));

 NSLog(@"Person结构体实际需要的内存大小: %zd", sizeof(PersonIMP));
 NSLog(@"Student结构体实际需要的内存大小: %zd", sizeof(StudentIMP));

得到的结果是:

Person对象实际需要的内存大小: 16
Student对象实际需要的内存大小: 16
Person结构体实际需要的内存大小: 16
Student结构体实际需要的内存大小: 24

可以看到,子类 Student 对象 实际需要的内存 并不是 对应的 结构体 StudentIMP所需要的内存。因为 OC 对象进行了优化。它会充分利用空间,所以子类和父类的成员 在内存上是连续的。也就是说,Student 真正对应的结构体在内存分布上应该是这样的:

struct StudentIMP_L { 
  Class isa; 
  int age; //!父类的属性 
  int no; 
};

这个时候,我们再进行结构体对齐,可以得出 实际所需内存 是 16个字节。

C++的结构体是也是可以继承的,OC虽然转化成C++结构体,但是内部还是做了很多的事情。

我们也可以通过 将 OC对象转换为 结构体进行验证。

 Student *stuent = [[Student alloc] init];
 stuent->age = 10;
 stuent->no = 20;
 
 struct StudentIMP_L *stuentIMP = (__bridge struct StudentIMP_L *)stuent;
 
 NSLog(@"age is %d, no is %d", stuentIMP->age, stuentIMP->no);

可以拿到打印结果:

age is 10, no is 20

这也侧面验证了,我们的对象的真实内存分部, 当然,我们也可以通过查看地址内存分部来获取,通过断点,获取当前对象

po stuent
<Student: 0x10076ebe0>

然后 view Memory 查看内存 0x10076ebe0 , 可以看到如下内存分布

39 22 00 00 01 80 1D 00 0A 00 00 00 14 00 00 00

0x0a 自然是 10,而 0x14 便是 20 了

x/4g 0x10076ebe0 指令也是可以的

属性优化

我们知道结构体里面的变量顺序是可以影响到整个结构体的内存分配的,那么我们通过调整 类的属性顺序 查看是否对其内存分配有影响,还是以 我们的 Person 为例子。定义如下:

{
  int age;
  double weight;
  int no;
}

那么转换成的结构体 如下:

struct Person_IMPL {
  struct NSObject_IMPL NSObject_IVARS;
  int age;
  double weight;
  int no;
};

按照结构体来看, 和前一个例子来看,这个对象的成员内存顺序 应该是 isa->age->weight->no
那么根据 结构体内存对齐原则,我们算出的 对象所需内存应该是 32。我们进行打印。

Person对象实际需要的内存大小: 32
Person对象实际分配的内存大小: 32

符合我们之前的推断。

但是如果我们换一个写法,如下:

@property (nonatomic, assign) int age;
@property (nonatomic, assign) double weight;
@property (nonatomic, assign) int no;

属性会生成对应的成员变量,那么理论上,我们猜测的顺序和之前应该是一样的,占用的内存应该也是 32

这个时候,我们将其转化为 C++ 代码,得到的结构体如下:

struct Person_IMPL {
  struct NSObject_IMPL NSObject_IVARS;
  int _age;
  int _no;
  double _weight;
};

很明显,我们的成员 _no 被放到的前面。那么根据 内存对齐原则,对象实际需要的内存变成了 24。 我们依然打印一下进行验证:

Person对象实际需要的内存大小: 24
Person对象实际分配的内存大小: 32

可以看到,OC对对象进行了优化,虽然对于这个例子,分配的内存没有改变,但是实际所需的内存变小了。通过合理安排成员顺序,减少了空间浪费,腾出了更多了额外空间,也就是尽量不产生碎片化内存,那么对象在新增一个属性的时候,可以尽量的少分配内存。

4. 测试案例

@interface Person : NSObject {
  int age;
  long width;
  long height;
}

@end

@implementation Person

@end


@interface Student : Person {
  int no;
}

@end

@implementation Student

@end

对于上述对象

  Person *per = [[Person alloc] init];
  Student *stu = [[Student alloc] init];

  NSLog(@"Person对象实际需要的内存大小: %zd", class_getInstanceSize([Person class]));
  NSLog(@"Student对象实际需要的内存大小: %zd", class_getInstanceSize([Student class]));

  NSLog(@"Person对象分配需要的内存大小: %zd", malloc_size((__bridge const void *)per));
  NSLog(@"Student对象分配需要的内存大小: %zd", malloc_size((__bridge const void *)stu));

打印

 Person对象实际需要的内存大小: 32
 Student对象实际需要的内存大小: 40
 Person对象分配需要的内存大小: 32
 Student对象分配需要的内存大小: 48

系统给其对象分配的内存已经截然不同了

而如果我们调整顺序

@interface Person : NSObject {
  long width;
  long height;
  int age;
}

@end

@implementation Person

@end


@interface Student : Person {
  int no;
}

@end

@implementation Student

@end

那么 系统分配的内存就有所减少

 Person对象实际需要的内存大小: 32
 Student对象实际需要的内存大小: 32
 Person对象分配需要的内存大小: 32
 Student对象分配需要的内存大小: 32