要想探究实例对象占用内存大小问题,首先要知道OC对象在具体的底层实现。借助clang
编译器提供的指令,将OC代码转换成C++代码来剖析具体的底层实现。
以下指令,将main.m文件转换成iOS系统下64位架构(以下讨论均在64位架构基础上进行)的C++代码。
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
首先我们讨论最简单的NSObject实例对象
创建一个NSObject类型实例。
NSObject *object = [[NSObject alloc] init];
在转换后的main-arm64.cpp文件中,我们会发现NSObject对象的底层实现就是下面的结构体:
struct NSObject_IMPL {
Class isa;
};
结构体中只有一个成员变量isa
,类型为指针。
那么,系统会为一个NSObject实例对象分配多少内存空间呢?
我们可以引入运行时框架来获取实例对象所占用的内存空间。
#import <objc/runtime.h>
NSLog(@"size_by_class_getInstanceSize --- %zd",
class_getInstanceSize([NSObject class]));// 8
这时我们可以看到控制台输出为8。很显然,一个isa
指针占8个字节,这样看起来好像没有问题?
其实不然,通过阅读运行源码可以看出,class_getInstanceSize
方法返回的是成员变量所占的内存空间,并不是系统实际为实例对象分配的空间大小。
使用malloc_size
函数可以获取系统到底为NSObject实例对象分配了多少内存空间。
#import <malloc/malloc.h>
NSLog(@"size_by_malloc_size --- %zd",
malloc_size((__bridge const void *)student));// 16
控制台打印16,这就是我们想要的答案。
为什么一个NSObject实例对象明明只需要8个字节的存储空间,系统却要为其分配16个字节的内存空间呢?这个答案在Foundtion框架的alloc
方法的底层实现中。
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个字节的存储空间。这就解释了为什么系统会分配16个字节给NSObject实例对象。
然后我们来看看稍微复杂一点的实例对象
定义一个Person类,继承自NSObject。Person类中定义两个int类型的成员变量_age
和_no
。
@interface Person : NSObject
{
@public
int _age;
int _no;
}
@end
@implementation Person
@end
创建一个Person实例对象
Person *person = [[Person alloc] init];
在转换之后的C++代码中,可以看到NSObject和Person对象各自的底层实现。
// NSObject
struct NSObject_IMPL {
Class isa;
};
// Person
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _no;
};
Person的第一个成员变量是NSObject_IMPL,也就是isa
指针。所以可以将Person底层实现写成下面的结构体
struct Person_IMPL {
Class isa;
int _age;
int _no;
};
我们再来分析一下系统会为一个Person实例分配了多少内存空间?
isa
指针类型占用8个字节,_age
整型占4个字节,_no
整型占4个字节,一共占用16个字节。
我们通过代码来验证一下。
NSLog(@"sizeby_class_getInstanceSize --- %zd",
class_getInstanceSize([Person class]));// 16
NSLog(@"sizeby_malloc_size --- %zd",
malloc_size((__bridge const void *)person));// 16
两次打印结果均为16,证明我们的推算没有问题。
最后我们来看更加复杂一点的实例对象
定义一个Student类,继承自Person。Student类中定义一个int类型的成员变量_weight
。
@interface Student : Person
{
@public
int _weight;
}
@end
@implementation Student
@end
创建一个Student实例对象。
Student *student = [[Student alloc] init];
同样,通过转换后的C++代码可以看到NSObject、Person和Student对象各自的底层实现。
// NSObject
struct NSObject_IMPL {
Class isa;
};
// Person
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _no;
};
// Student
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
int _weight;
};
推导出Student底层实现的本质就是
struct Student_IMPL {
Class isa;
int _age;
int _no;
int _weight;
};
同样,我们再来推测一下系统会为一个Student实例对象分配多少内存空间?
isa
指针类型占用8个字节,_age
整型占4个字节,_no
整型占4个字节,_weight
整型占4个字节,共占用20个字节。我们可以通过代码来验证一下。
NSLog(@"size_by_class_getInstanceSize --- %zd",
class_getInstanceSize([Student class]));// 24
NSLog(@"size_by_malloc_size --- %zd",
malloc_size((__bridge const void *)student));// 32
看到控制台打印
size_by_class_getInstanceSize --- 24
size_by_malloc_size --- 32
一脸蒙蔽!尼玛一个都没对!
这时候,就涉及到了内存对齐的问题。
首先解释class_getInstanceSize
的结果为什么是24。
为结构体分配内存空间时,所分配的内存空间大小必须是结构体中占用最大内存空间成员所占用内存大小的倍数。
在上面例子中,占用最大内存空间成员为isa
指针,占用8个字节,所以,能容纳20个字节的数据,最小的8的倍数就是24。
再来,malloc_size
的结果为什么是32。
iOS操作系统本身,在分配内存时也做了内存对齐的处理,规定分配内存大小必须是16的倍数,所以结果为32。