了解OC内存分配可以从多个维度进行分析,首先用最简单地基类NSObject来分析,然后在拓展到其他复杂的类。
一:通过api获取一个实例变量的大小
如下obj占用多少内存呢?
NSObject *obj = [[NSObject alloc] init];
其实获取一个对象占用多少内存有苹果提供对应的api
引入头文件
#import <objc/runtime.h>
#import <malloc/malloc.h>
// 获得NSObject实例对象的成员变量占用的大小,结果为8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
// 获得obj指针所指向内存实际占用大小,结果为16
NSLog(@"%zd", malloc_size((__bridge const void *)obj));
这两个结果为啥不一样呢?单纯从方法名字来看class_getInstanceSize
代表获取实例所占用大小,malloc_size
代表开辟多少空间,其实应该是一样的才对,但是如果看苹果源码就会看出区别来。
1.下面探索class_getInstanceSize
为什么小于malloc_size
大小。
苹果objc源码传送门,然后搜索"objc",找到"objc4/"点击进去,然后选取编号比较大的(最大的编号代表是最新的源码)。我下载的是"objc4-818.2.tar"。解压打开工程搜索class_getInstanceSize
,找到实现如下:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
class_getInstanceSize
继续查找如下:
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
从上边注释就可以看出来,class_getInstanceSize
实际获取的是成员变量所占用存储空间的大小,word_align
这个是字节对齐(注意后边要用到)。
到这里就可以看出class_getInstanceSize
获取的大小实际就是objc
实例对象成员变量所占用空间的总和。那么一个NSObject的实例变量objc
到底有多少成员变量呢?
2.获取OC对应的C/C++代码
OC对象其实底层都是C/C++,所以想要更加明白的看清楚OC对象的本质就是把OC代码转成成C/C++。
OC的的面向对象其实就是基于C/C++的结构体封装的。
可以这样做,在终端执行如下命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 将要输出的文件
。
参数解释:
iphoneos
代表iPhone
-arch arm64
表示将OC源码翻译成arm64架构支持的格式。armv7(3位)、i386(模拟器)这些就不需要了。现在iPhone都是64位的所以只转换64位就够了。
比如我的OC代码是这样的:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"stu - %zd", class_getInstanceSize([Student class]));//8
NSLog(@"stu - %zd", malloc_size((__bridge const void *)stu));//16
}
return 0;
}
经过转换以后打开得到的C++代码可以看到有这样的一段代码:
struct NSObject_IMPL {
Class isa; // 8个字节
};
其实这个就是NSObject
对象的本质,是一个结构体。里边只有一个变量就是isa
,类型为Class
。而Class
其实是一个结构体指针typedef struct objc_class *Class;
。所以isa占用8个字节。
到这里就可以看出来为什么class_getInstanceSize
获取到的数字为8了,因为一个NSObject实例对象只有一个成员变量,并且这个成员变量占用8个字节。
3.接下来分析malloc_size
获取到的为什么是16?
因为一个OC的实例对象是使用alloc来分配内存的,底层是执行的allocWithZone
,所以直接在源码中搜索allocWithZone
。
// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
继续查找 _objc_rootAllocWithZone
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
if (fastpath(!zone)) {
obj = class_createInstance(cls, 0);
} else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
if (slowpath(!obj)) obj = _objc_callBadAllocHandler(cls);
return obj;
}
继续查找 _class_createInstancesFromZone
/***********************************************************************
* _class_createInstancesFromZone
* Batch-allocating version of _class_createInstanceFromZone.
* Attempts to allocate num_requested objects, each with extraBytes.
* Returns the number of allocated objects (possibly zero), with
* the allocated pointers in *results.
**********************************************************************/
unsigned
_class_createInstancesFromZone(Class cls, size_t extraBytes, void *zone,
id *results, unsigned num_requested)
{
unsigned num_allocated;
if (!cls) return 0;
size_t size = cls->instanceSize(extraBytes);
num_allocated =
malloc_zone_batch_malloc((malloc_zone_t *)(zone ? zone : malloc_default_zone()),
size, (void**)results, num_requested);
for (unsigned i = 0; i < num_allocated; i++) {
bzero(results[i], size);
}
// Construct each object, and delete any that fail construction.
unsigned shift = 0;
bool ctor = cls->hasCxxCtor();
for (unsigned i = 0; i < num_allocated; i++) {
id obj = results[I];
obj->initIsa(cls); // fixme allow nonpointer
if (ctor) {
obj = object_cxxConstructFromClass(obj, cls,
OBJECT_CONSTRUCT_FREE_ONFAILURE);
}
if (obj) {
results[i-shift] = obj;
} else {
shift++;
}
}
return num_allocated - shift;
}
继续查找 size_t size = cls->instanceSize(extraBytes);
中的函数 instanceSize
的实现
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;
}
在最后一个方法instanceSize
中可以看到这么一段
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
要求所有的对象最少是16字节。所以这也就解释了为什么通过class_getInstanceSize
获取到的大小明明只有8个字节,而通过malloc_size
获取到的实际开辟空间为16了。这个16字节是苹果的一种规则,也是结构体内存对齐的规则。结构体内存对齐有一条规则是结构体的总大小是内部占用最大内存类型的整数倍。
二:自定义类深入了解空间分配
下面定义一个Person 和 Student类,如下:
@interface Person : NSObject
{
@public
int _age;
}
@interface Student : Person
{
@public
int _no;
}
@end
源码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
NSLog(@"person - %zd", class_getInstanceSize([Person class]));//16
NSLog(@"person - %zd", malloc_size((__bridge const void *)person));//16
NSLog(@"%zd", sizeof(struct Student_IMPL));//16
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);
Student *stu = [[Student alloc] init];
NSLog(@"stu - %zd", class_getInstanceSize([Student class]));//16
NSLog(@"stu - %zd", malloc_size((__bridge const void *)stu));//16
}
return 0;
}
其实上边获取到的这两个实例对象通过class_getInstanceSize
和通过malloc_size
获取到的结果都是16,这又是为什么?
老规矩可以通过源码分析。
同样的方法获取到的C++源码中有这么一段代码:
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVAS;
int _age;
};
struct Student_IMPL {
struct Person_IMPL Person_IVAS;
int _no;
};
其实可以把这些代码合并简化一下就是这样的:
struct NSObject {
Class isa;
};
struct Person {
Class isa;
int _age;
};
struct Student {
Class isa;
int _age;
int _no;
};
这样看起来就很简单了吧。
1.首先分析Person
实例对象
isa
;8个字节
_age
;4个字节
这样看起来应该是12字节才对,但是上边提到了class_getInstanceSize
方法里边会进行内存对齐,所以结果为16。
malloc_size
分配最少为16字节。所以这里是16.
2.分析Student
实例对象
isa
;8个字节
_age
;4个字节
_no
;4个字节
这样看起来刚好16字节,所以结果为16.
malloc_size
分配最少为16字节,但是单单一个Person就已经占用了16字节了,_no
是不是要存放在其他的空间里边?其实不需要的,因为前两个成员变量isa
、_age
才占用了12字节,还剩余4个字节的空间,这个是不能浪费的,又因为int 类型的_no
刚好占用4个字节,所以剩余的4个字节刚好用来存放_no
。所以结果还是为16。
上边使用sizeof()
获取Student对应的结构体结果和class_getInstanceSize
是一样的,只不过sizeof()
是在编译器就计算出了结果,而class_getInstanceSize
是在运行时计算的。
把stu
对象强转成Student_IMPL
然后进行访问也可以得到正确的结果。说明stu
数据结构确实和Student_IMPL是一样的。
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
画了一张图如下:

3.分析Student增加一个属性
Student
如下:
@interface Student : NSObject
{
@public
int _no;
}
@property (nonatomic, assign) int weight;
@end
这时候student
对象占用空间又是怎么样的呢?
NSLog(@"student - %zd", class_getInstanceSize([Student class]));//24
NSLog(@"student - %zd", malloc_size((__bridge const void *) student));//32
首先还是转换C/C++代码。
如下:
struct Student {
Class isa;
int _age;
int _no;
int _weight;
};
class_getInstanceSize
为什么是24?
isa
;8个字节
_age
;4个字节
_no
;4个字节
_weight
;4个字节
累计占用20字节,因为存在内存对齐,所以总共需要24字节。
malloc_size
最少为16,但是每次开辟空间都是16的倍数,所以大小为32。
4.增加了属性以后,为什么只增加了成员变量没有增加方法?
成员变量每个实例对象会存储一份,因为每个实例对象的同一个变量存储的数据是不同的,但是方法是公用的没必要每个实例对象保存一份。所以实例方法存放在类里边,类方法会存放在元类里边,这些以后专门写篇文章介绍。
三:可视化工具查看
除了代码分析以外还可以使用xcode提供的可视化工具查看内存分布。
Debug -> Debug Workflow -> View Memory可以大致查看内存分布状况。
代码如下:
Person *person = [[Person alloc] init];
person.weight = 3;
person->_age = 4;
内存分布图如下

从图中可以看到_age 为04 00 00 00,weight为 03 00 00 00,因为iPhone使用的是小端模式,从高内存往低内存读取数据,其实读取到的顺序是00 00 00 04, 00 00 00 03。
关于OC内存分配大致说这么多吧。
网友评论