NSObject是所有类的基类,所有的类都继承自它,它太平常了,平常到我们从不去多加任何思考,但是它又那么重要,因为他是OC的基础,所以今天我们将通过一下几点对NSObject
抽丝剥茧,看看他的底层到底是怎样的:
- 一个 NSObject 对象占用多少内存?
- 对象的 isa 指针指向哪里?
- OC 的类信息存放在哪里?
思考一下:一个 OC 对象在内存中是如何布局的?
我们创建一个简单 Command line 项目,然后通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -0 main.cpp
命令把 .m 文件转换为 c++文件查看一下底层代码,然后在转换后的.cpp
文件中搜索struct NSObject_IMPL {
找到:
struct NSObject_IMPL {
__unsafe_unretained Class isa;
};
这就是NSObject
的底层实现,我们也可以直接进入到NSObject.h
的头文件中看看NSObject
是如何定义的:
@interface NSObject {
Class isa;
}
@end
可以发现NSObject.h
头文件的定义和.cpp
文件中都是一样的,NSObject
的底层就是一个 C++ 结构体,结构体中只有一个Class
类型的isa
成员.这个class
又是什么类型呢?点进去看一下:
typedef struct objc_class *Class;
原来class
就是一个指向struct objc_class
结构体类型的指针!现在知道了Class
是一个指针,而NSObject
底层就只有一个Class isa
那我们就知道了NSObject
占用多少内存了.因为指针在64位系统中占8个字节,在32位系统中占4个字节.所以我么可以猜测:NSObject
在内存中占8个字节.我们猜测的正确吗?下面开始验证一下:
NSLog(@"NSObject 占用了 %zd 个字节?", class_getInstanceSize([NSObject class]));
//打印输出
NSObject 占用了 8 个字节?
难道我们的猜测是正确的?
注意:class_getInstanceSize()
是获取某一个类创建出来的实例对象所占用的内存大小.
系统中还有一个方法是取出一个指针所指向的内存的大小:malloc_size(<#const void *ptr#>)
运行一下代码:
NSObject *obj = [[NSObject alloc]init];
NSLog(@"NSObject 占用了 %zd 个字节?", class_getInstanceSize([NSObject class]));
NSLog(@"obj指针指向的内存占用了 %zd 个字节?",malloc_size((__bridge const void *)obj));
// 打印
NSObject 占用了 8 个字节?
obj指针指向的内存占用了 16 个字节?
好了,现在有两个结果:8 和 16.哪一个才一个 NSObject 对象占用多少内存?
的结果呢?答案是16个字节.因为class_getInstanceSize ()
返回的其实并不是一个对象的全部内存大小,实际上它返回的是一个类的实例对象的成员变量所占用的内存大小,我们可以通过 runtime 的源码看一下:
查看步骤:
- 打开 runtime 源码搜索
class_getInstanceSize
- 找到
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
- 点击进入
alignedInstanceSize
:
// Class's ivar size rounded up to a pointer-size boundary.
翻译:返回的是class的ivar大小
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
可以看到, class_getInstanceSize()
的确返回的是实例对象的成员变量所占用的大小.其实 class_getInstanceSize
和 malloc_size
的关系就好比下图:
所以正确的说法应该是:
一个 NSObject 对象占用多少内存❓
系统分配了 16 个字节给NSObject
对象(通过malloc_size
可获得),但NSObject
对象内部只是用了 8 个字节的空间(在64位环境下可通过class_getInstanceSize
函数获得).
我们还可以从 runtime 底层代码查看 NSObject
的alloc
方法来验证刚刚得出的结论.
验证步骤:打开runtime
源码 --> 搜索allocWithZone
--> 点击进入class_createInstanceFromZone
方法 --> 点击进入 _class_createInstanceFromZone
可以看到创建的alloc
方法是调用obj = (id)calloc(1, size);
传入了一个size,而这个size
是调用instanceSize(extraBytes)
获得,我们再进入instanceSize(extraBytes)
内部,它的底层实现是这样的:
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;//小于 16 ,就让他等于 16.
return size;
}
通过底层源码我们可以看到,如果 size < 16,就让他 等于 16,所以,NSObject
的内存最小是16个字节.
我们还可以通过 view Memory 侧面查看一下NSObject
的内存地址:
可以看到
obj
所占用的16个字节中,前8个字节是有值的,后8个字节全是0,其实前8个字节中存放的就是我们上面分析的isa
.当然,这种方法不太严谨,了解一下就行.如果不想通过 view Memory工具查看,还可以使用命令行查看,先介绍几个常用的 LLDB 命令:
- 打印
print , p : 打印
po : print object简写,打印对象- 读取
memory read 等于 x : 读取内存
比如: x/4xg 0x10086 : 读取 0x10086内存地址,显示4段,每段表示8个字节,以16进制显示.
这里的 4 代表: 数量
x 代表: 格式(x是16进制,f是浮点,d是10进制)
g 代表: 字节大小(b:是byte,表示1字节;h:是half word,表示2字节;w:是word,表示4个字节;g:是giant word,表示8个字节)- 修改
memory write 内存地址 数值: memory write 0x10086 18
我们使用 LLDB 命令打印一下obj
地址:
我们在项目中肯定是使用自定义的类,所以我们拓展一下,定义两个类
Person,Student
// Person
@interface Person : NSObject
{
@public
int _no;
}
@end
@implementation Person
@end
// Student
@interface Student : Person
{
int _age;
}
@end
@implementation Student
@end
然后再实例化这两个类,打印输出它们占用的内存大小:
Person *person = [[Person alloc]init];
NSLog(@"Person 占用了 %zd 个字节?", class_getInstanceSize([Person class]));
NSLog(@"person指针指向的内存占用了 %zd 个字节?",malloc_size((__bridge const void *)person));
Student *student = [[Student alloc]init];
NSLog(@"student 占用了 %zd 个字节?", class_getInstanceSize([Student class]));
NSLog(@"student指针指向的内存占用了 %zd 个字节?",malloc_size((__bridge const void *)student));
大家可以猜测一下打印的结果是什么?
我们分析一下:Person
继承NSObject
,它的底层应该是这样:
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; //指针,占8个字节
int _no;//4个字节
};
所以class_getInstanceSize
结果应该是12个字节,malloc_size
结果应该是16个字节,因为内存对齐的原则.
而Student
继承自Person
,它的底层应该是这样:
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
int _age;
};
那Student
的class_getInstanceSize 和 malloc_size
输出结果应该是多少呢?
我们直接来看一下运行结果:
Person 占用了 16 个字节?
person指针指向的内存占用了 16 个字节?
student 占用了 16 个字节?
student指针指向的内存占用了 16 个字节?
可以看到打印的都是16个字节,我们刚才分析的Person
的class_getInstanceSize
结果应该是12个字节呀?怎么输出的是16个字节?ok,我们从runtime
源码中寻找答案,打开runtime
源码找到class_getInstanceSize
底层实现:
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
align
是对齐的意思,通过代码我们可以看出来,传入一个没有对齐的大小返回内存对齐后的大小.现在我们应该明白了class_getInstanceSize
为什么是16而不是12了,因为这是内存对齐后的结果.
为什么student
也是输出16个字节呢?因为通过以上分析我们知道Person
占用了12个字节,但是系统给person
分配了16个字节,还有4个自己接是空闲的,而student
内部有一个int _age
占用4个字节,系统当然不会放着4个空闲的字节不用,再去开辟内存,所以结果就是 12 + 4 = 16 个字节.
思考一下,如果再给student
增加一个成员变量int _height
,student
的内存会有什么变化呢?
Person 占用了 16 个字节?
person指针指向的内存占用了 16 个字节?
student 占用了 24 个字节?
student指针指向的内存占用了 32 个字节?
输出结果如上所示,增加了int _height
后,student
的内存应该是 Person
中的 NSObject_IVARS 8 个字节
+ _no 的 4 个字节
也就是12 + 4 + 4 = 20,但是class_getInstanceSize
输出的确是24,因为一个实例对象的大小必须是它最大成员变量的倍数,student
最大的成员变量大小是8 (NSObject_IVARS
),所以它的倍数就是24,而malloc_size
字节对齐的规则是必须是16的倍数,所以malloc_size
结果是32.