先上一道面试题:
一个NSObject对象占用多少内存?
想要弄清这个问题,那么我们如果知道了一个NSObject对象在内存中是如何布局的,就知道了这个对象占用了多少内存。
想必小伙伴们都知道,OC作为C语言的超集,它的面向对象的能力是以C/C++
来实现的。Xcode也是支持将OC转成C++代码的:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
OC对象的底层实现
了解了如何将OC代码重写为C++代码,我们首先看一下在NSObject.h
中是如何定义NSObjcet
的呢?
// NSObject.h
@interface NSObject <NSObject> {
Class isa;
}
从框架的头文件中可以看到,NSObject
只有一个isa
的成员变量。我们将如下代码重写为C++然后观察一下。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}
细心的话可以发现的这样一段代码:
struct NSObject_IMPL {
Class isa;
};
从结构体的名称就可以看出,NSObject
在底层被重写为一个结构体。只有一个成员变量isa
。而NSObjcet
作为OC中所有类的基类,我们可以得出一条结论:OC对象在底层是用结构体来实现的。
我们还可以追溯Class
的定义:
typedef struct objc_class *Class;
所以Class
类型,是一个指向objc_class
结构体的指针。同时我们知道,一个指针在64位系统上占用8个字节,32位系统上占用4个字节。所以我们可以推断出,**只有一个成员变量的结构体NSObject_IMPL
所占的内存大小就是isa
所占的内存空间空间大小。**即64位环境下:一个NSObject对象占8个字节。
但实际上不是,实际上是16个字节。
通过API来获取对象大小
先介绍两个用来获取对象大小的函数
// 通过类型来获取该类型实例对象所占的内存大小
#import <objc/runtime.h>
size_t class_getInstanceSize(Class _Nullable cls);
// 通过一个指针来获取该指针指向内存的大小
#import <malloc/malloc.h>
size_t malloc_size(const void *ptr);
我们通过这两个函数来获取一下NSObject
的实例对象所占内存的大小:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
// >>8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
// >>16
NSLog(@"%zd", malloc_size((__bridge const void *)obj));
}
return 0;
}
那么系统到底为这个obj
对象分配了多少内存呢?我们从NSObject
的alloc
着手分析一下。
在源码中,我们可以看到alloc
的实现:调用了_obj_rootAllic()
,
//NSObject.mm
+ (id)alloc {
return _objc_rootAlloc(self);
}
继续搜索_obj_rootAllic()
,其实现为:
//NSObject.mm
id _objc_rootAlloc(Class cls) {
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
继续搜索callAlloc
函数,其实现为:
//NSObject.mm
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
搜索_objc_rootAllocWithZone
的实现:
//NSObject.mm
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
搜索_class_creatInstanceFromZone
的实现:
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;
}
从这里我们可以看到,到_class_creatInstanceFromZone
中:
size_t size = cls->instanceSize(extraBytes);
,而instanceSize
中:
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;
}
可以看到 如果size最小为16,所以NSObject
的实例对象占用16个字节是源代码中影响规定的,而实际上,一个NSObject
只用了8个字节(64位)。
整体的调用流程为:
API结果的含义
根据我们前面一系列的探索,我们最终得知,一个NSObject
对象系统分配了16个字节。
前面我们使用的两个函数,也应该知道了获取的真正含义:
malloc_size()
:创建一个实例对象,实际上分配了多少内存。class_getInstanceSize()
:创建一个实例对象,至少需要多少内存(结果等同于sizeof()
运算符)
sizeof()
的注意点:
sizeof
是一个运算符,并不是一个函数。传入一个类型,返回该类型的所占用的内存。在编译的时候就编译为常数。假如传入一个指针连变量,则返回这个指针变量所占的内存大小。至于这个指针指向内存的大小,则需要
malloc_size()
。
NSObject的子类
通过前边我们了解了NSObject
的底层实现,以及实例对象所占内存的大小。那么对于NSObject
的子类情况又是如何呢?
我们先自定义一个Student
类
@interface Student : NSObject {
@public
int _no;
int _age;
}
@end
@implementation Student
@end
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
我们将其重写为C++,可以看到Student
在底层的实现为:
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS; //父类的结构体
int _no;
int _age;
};
可以看到底层的结构体,第一个成员为父类的结构体,其余依次为自身成员属性。
在代码中显示声明结构体Student_IMPL
,也可以将Student
对象转换为Student_IMPL
结构体:
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
struct Student_IMPL *stu2 = (__bridge struct Student_IMPL *)stu;//OC转C,需要使用__bridge
NSLog(@"%d,%d",stu2->_no,stu2->_age);//4,5
那么Student
的实例对象占用多少内存呢?
我们打一个断点,来获取代码中stu
的内存地址:0x100713890
。通过Xcode Debug -> Debug Workflow -> View Memory来查看实时内存。我们大概的可以推断出Student
也占16个字节。
从Student
在底层转换实现的结构体可以看出,Student_IMPL
结构体一共有三个成员变量,其中NSObject_IVARS
分配了16个字节,但是只是用了8个字节,还有八个字节,正好可以存放两个int
型变量。
// >>16
NSLog(@"%zd",malloc_size((__bridge const void *)stu));
// >>16
NSLog(@"%zd",class_getInstanceSize([Student class]));
再多一个成员
当然如果Student
的再多一个int
成员变量:
@interface Student : NSObject
{
@public
int _no;
int _age;
int _height;
}
@end
@implementation Student
@end
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
stu->_height = 6;
// >>32
NSLog(@"%zd",malloc_size((__bridge const void *)stu));
// >>24
NSLog(@"%zd",class_getInstanceSize([Student class]));
// >> 24
NSLog(@"%zd",sizeof(struct Student_IMPL));
我们可以看到系统分配的空间为32个字节,实例所占的内存空间而是24个字节。这就涉及到了iOS系统底部内存优化和内存对齐的问题。通过跟踪alloc
函数,程序最终调用了:
obj = (id)calloc(1, size); // size即24
可以看出,这个对象至少需要24个字节。当向系统申请24个字节内存的时候,系统分配了32个字节。通过在libmalloc
开源代码中,了解到iOS系统分配内存都是16的倍数(推测是加快内存分配速度和访问速度)。所以系统向其分配了32个字节。
更为复杂的场景
现在我们假设有这样的场景:
现在有两个类: Student
和Person
。其中他们的继承关系为:Student
-> Person
-> NSObject
。
@interface Person : NSObject
{
@public
int _age;
}
@end
@interface Student : Person
{
@public
int _no;
}
@end
那么Person实例对象,Student的实例对象分别占用多少内存呢?。答案都是16个字节。
我们知道,Person
和Student
对象在底层都是这样实现的:
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; //8个字节
int _age; //4个字节
}; //16个字节:结构体的大小必须是最大成员大小的倍数(内存对齐)。
struct Student_IMPL {
struct Person_IMPL Person_IVARS;//16个字节,但是空余4个字节
int _no; //4个字节,因为前边正好有4个空余的字节,正好用来存放_no。
};
我们来证实一下:
Person *p = [[Person alloc] init];
// >>16,这里从上面的分析来看应该返回12,但是这个函数是返回对齐后的内存大小。
NSLog(@"Person:%zd",class_getInstanceSize([Person class]));
// >>16
NSLog(@"p:%zd",malloc_size((__bridge const void *)p));
Student *stu = [[Student alloc] init];
// >>16
NSLog(@"Student:%zd",class_getInstanceSize([Student class]));
// >>16
NSLog(@"stu:%zd",malloc_size((__bridge const void *)stu));
@property 成员
对于@property
修饰的成员变量,编译器自动生成成员变量:_
+ 变量名;并且生成setter
&getter
方法。
OC实现:
@interface Person : NSObject
{
@public
int _age;
}
@property (nonatomic,assign) int height;
@end
底层实现:
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
};
static int _I_Person_height(Person * self, SEL _cmd) {
return (*(int *)((char *)self + OBJC_IVAR_$_Person$_height));
}
static void _I_Person_setHeight_(Person * self, SEL _cmd, int height) {
(*(int *)((char *)self + OBJC_IVAR_$_Person$_height)) = height;
}