小码哥底层原理笔记:OC对象的本质
(默认64位系统下)
OC的对象结构都是通过基础C\C++的结构体实现的。
NSObject的本质
我们先看一个简单对象NSObject:
int main(int argc, char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}
我们通过命令行将OC的mian.m文件转化为c++文件
clang -rewrite-objc main.m -o main.cpp // 这种方式没有指定架构例如arm64架构 其中cpp代表(c plus plus)
生成 main.cpp
我们可以指定架构模式的命令行,使用xcode工具 xcrun
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
生成 main-arm64.cpp
在main-arm64.cpp 文件中搜索NSObjcet,可以找到NSObjcet_IMPL(IMPL代表 implementation 实现)代码如下:
struct NSObject_IMPL {
Class isa; //8个字节
};
//指针
typedef struct objc_class *Class;
可见一个NSObject对象其实就是一个NSObject_IMPL结构体。
通过以下代码打印得知一个NSobject类实例对象的大小是8字节,也就说NSObject_IMPL结构体大小是占8字节,由于NSObject_IMPL结构体中只有一个isa指针成员变量,所以一个isa指针占8个字节。
//获得NSObject类实例对象的大小:8个字节
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
我们再通过以下代码看到iOS系统实际分配了16个字节给这个NSobject对象,但是NSobject对象实际只用了8个字节
//获得obj指针所指向的内存大小:16个字节,虽然分配了16但是实际只是用了8个字节
NSLog(@"%zd", malloc_size((__bridge const void *)obj));
当然方法也占用内存空间,但是实例对象并不保存方法,方法存在其他地方。
自定义类Person的本质
Person对象继承自NSObject
@interface Person : NSObject
{
int _age;
int _height;
int _no;
}
@end
@implementation Person
@end
同理,我们转化成C++代码如下:
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;//占8字节,即:Class isa;
int _age;//占4字节
int _height;//占4字节
int _no;//占4字节
};//20字节,内存占用大小应该是8的倍数,所以最终是占24个字节
因为OC对象的本质就是一个结构体,所以结构体占用多少自己,对象就占用多少字节。
我们分析,一个Person对象总共有4个成员变量,父类NSObject的NSObject_IMPL占8个字节,三个int成员变量分别占4个字节,所以加一起Person对象一共占20字节,但是内存占用大小应该是8的倍数,所以最终打印出来是占用24个字节。
NSLog(@"%zd", sizeof(struct Person_IMPL));//24结构体实际所占大小
Person *person = [[Person alloc] init];//实例对象的方法不存放方法
NSLog(@"person - %zd", class_getInstanceSize([Person class]));//24,对象实际所占大小
但正常的OC对象的属性是@property,如下代码:
@interface Person : NSObject
{
int _age;
}
@property (nonatomic, assign) int height;//也是转化为成员变量保存
@end
实际上上述代码相当于:
@interface Person : NSObject
{
int _age;
int _height;
}
///拆分成成员变量和get,set方法,这里的方法不保存在实例对象中,保存在对象的分类中。因为方法只需要保存一份就行,如果保存在实例对象中,那么每次alloc初始化一个实例对象就要copy一份方法。这样就浪费内存了。
- (void)setHeight{
_height = 0;
}
- (int)getHeight{
retutrn _height;
}
@end
我们再来看一个更复杂一点的
/* Person */
@interface Person : NSObject
{
int _age;
}
@end
@implementation Person
@end
/* Student */
@interface Student : Person
{
int _no;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%zd %zd",
class_getInstanceSize([Person class]),
class_getInstanceSize([Student class])
);
//实际分配了多少字节
NSLog(@"person - %zd student - %zd", malloc_size((__bridge const void *)person),malloc_size((__bridge const void *)stu));
}
return 0;
}
打印结果:
16 16
person - 16 student - 16
我们依据上面的分析与发现,类对象实质上是以结构体的形式存储在内存中,画出真正的内存图例
实际Person和Student对象在内存中都是以结构体的形式存储,看右半部分图可知。
我们发现只要是继承自NSObject的对象,那么底层结构体内一定有一个isa指针。因为NSObject实际上就是一个只包含isa指针的结构体
接下来我们分析Person实例对象中,NSObject_IMPL占用8个字节,_age占4个字节,由于一个OC对象至少占16字节,所以Person实例对象占16字节。在Student实例对象中Person_IMPL占12个字节,_no占4个字节,所以Student实例对象也是占16字节。
再来一个例子:
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;//占8字节
int _age;//占4字节
int _height;//占4字节
int _no;//占4字节
};//20字节,内存占用大小应该是8的倍数,所以最终是占24个字节
@interface Person : NSObject
{
int _age;
int _height;
int _no;
}
@end
@implementation Person
@end
@interface Student : Person
{
int _weight;
}
@end
@implementation Student
@end
struct Student_IMPL
{
struct Person_IMPL Person_IVARS;
int _weight;
};
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"%zd", sizeof(struct Person_IMPL));//24结构体实际所占大小
Person *person = [[Person alloc] init];//实例对象的方法不存放方法
NSLog(@"person - %zd", class_getInstanceSize([Person class]));//24,对象实际所占大小
NSLog(@"person - %zd", malloc_size((__bridge const void *)person));//32, OC分配的大小,OC分配给对象的内存是16的倍数
}
return 0;
}
注意:class_getInstanceSize方法是获取实例对象实际占用的内存大小,malloc_size方法是获取系统分配给对象的内存大小
内存对齐原则
1、实际所占内存大小应该是8的倍数,例如算出来的实际所占大小是24,补齐之后应该是24。
2、iOS系统分配给对象的内存大小最少是16字节,并且是16的倍数,例如实际对象占用内存24,但是系统会分配32。
注:Double类型占8字节,其他如NSString,NSNumber等对象占16字节
面试题
一个NSObject对象占用多少内存?
答:系统分配了16个字节给NSObject(通过malloc_size函数获得),但是NSObject对象内部只使用了8个字节的空间(通过class_getInstanceSize函数获得)(64位系统下)