OC实例对象的本质
- 我们平时编写的OC代码,底层实现其实都是C/C++代码,所以OC的面向对象都是基于C/C++的数据结构实现的,比如:OC里的对象、类主要是基于C/C++的结构体
Struct数据结构实现的。
-
将OC代码转换为C/C++代码:
➜ clang -rewrite-objc main.m -o main.cpp -
由于不同平台的硬件及系统都不一样,所以支持的代码肯定也是不一样的。由于汇编语言严重依赖于硬件的,而汇编语言又是由C/C++转换过来的,所以同一段OC代码,要想运行在iOS和Mac平台上,首先要转换成不同的C/C++代码。
➜ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp-arch指定对应平台具体的架构,针对iOS平台,模拟器(i386)、32bit(armv7)、64bit(arm64)
一个NSObject对象占用多少内存?
在 main.m 里创建一个 NSObject 对象:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}
通过查看 NSObject.h 文件来查看 NSObject 对象的定义:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
将OC代码转换成C/C++代码,搜索 NSObject_IMPL,可以看到 NSObjcect 对象的底层实现是:
// NSObject Implementation
struct NSObject_IMPL {
Class isa;
};
// Class这个对象是指向结构体的指针
typedef struct objc_class *Class;
NSObject 通过 alloc 方法去分配内存空间,而 NSObject 的实例对象 obj 中,只有一个 isa 指针,占8个字节(64位),所以按理说,obj 所占用的内存空间也是8个字节,但实际上 obj 占用了16个字节的内存空间。
通过分析源代码去验证
NSObject *obj = [[NSObject alloc] init]; // 16个字节
// 获得NSObject实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
// class_getInstanceSize 表示创建一个实例对象,至少需要多少内存?
// 获得obj指针所指向内存的大小 >> 16
NSLog(@"%zd", malloc_size((__bridge const void *)obj));
// malloc_size 表示创建一个实例对象,实际上分配了多少内存?
探寻 class_getInstanceSize 源码:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
// 由注释可以看到,这里返回的是类的成员变量所占用的大小(内存对齐过的)
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
探寻 alloc 源码:
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}
id
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
// 获取实例大小
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size); // 根据size为obj分配内存空间
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
// 可以看到,在CoreFoundation框架中,规定了对象至少要占16个字节
if (size < 16) size = 16;
return size;
}
那么一个NSObject对象到底占用了多少内存呢?
- 系统分配了16个字节给NSObject对象(通过
malloc_size函数获得) - 但是NSObject对象内部只使用了8个字节的内存空间(64bit 环境下,通过
class_getInstanceSize函数获得) - 实例对象在内存中只有成员变量,没有存储方法,因为不同的实例对象的成员变量的值可以是不同的,但是他们的方法都是一样的,没必要重复存储,只需要在 meta-class 元类对象中存储一份即可。
实时查看内存数据
可以看到,obj 在内存中的地址,根据这个地址,我们可以看到该实例对象在内存中的具体情况。
通过Xcode Debug工具
Debug -> Debug Workfllow -> View Memory (Shift + Command + M)
之后根据 obj 的地址,我们可以查看到该对象在内存中的具体存储情况:
一个16进制位代表4个二进制位,两个16进制位就代表8个二进制位,也就是一个字节。可以看到,自目标地址往后的16个字节中存储的就是 obj 对象,前八个字节里存储了数据,后八个字节为空。
通过LLDB指令
常用LLDB指令:
Printing description of obj:
<NSObject: 0x100604260>
(lldb) print obj
(NSObject *) $0 = 0x0000000100604260
(lldb) p obj
(NSObject *) $1 = 0x0000000100604260
(lldb) po obj
<NSObject: 0x100604260>
(lldb) memory read 0x100604260
0x100604260: 19 51 3d 98 ff ff 1d 00 00 00 00 00 00 00 00 00 .Q=.............
0x100604270: 00 00 88 80 00 00 00 00 a0 4d 3d 00 01 00 00 00 .........M=.....
(lldb) x 0x100604260
0x100604260: 19 51 3d 98 ff ff 1d 00 00 00 00 00 00 00 00 00 .Q=.............
0x100604270: 00 00 88 80 00 00 00 00 a0 4d 3d 00 01 00 00 00 .........M=.....
(lldb) x/3xg 0x100604260
0x100604260: 0x001dffff983d5119 0x0000000000000000
0x100604270: 0x0000000080880000
(lldb) x/4xw 0x100604260
0x100604260: 0x983d5119 0x001dffff 0x00000000 0x00000000
(lldb) memory write 0x100604269 9
(lldb) x 0x100604260
0x100604260: 19 51 3d 98 ff ff 1d 00 00 09 00 00 00 00 00 00 .Q=.............
0x100604270: 00 00 88 80 00 00 00 00 a0 4d 3d 00 01 00 00 00 .........M=.....
更复杂的情况
自定义 Student 类型,继承自 NSObject,包含两个 int 类型的成员变量,其实例对象会占用多少内存?
// 实际的底层结构体实现
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
// => 16
NSLog(@"%zd", class_getInstanceSize([Student class]));
// => 16
NSLog(@"%zd", malloc_size((__bridge const void *)stu));
// 强制将 Student 对象转换为 Student_IMPL 结构体
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
// => no is 4, age is 5
NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);
}
return 0;
}
我们自定义的 Student 对象实际底层会有三个成员变量,isa 指针占用8个字节,_no 占用4个字节 _age 占用4个字节,由于一开始在分配内存的时候,就分配了16个字节,而所有成员变量所需内存空间加起来正好等于16个字节,所以 class_getInstanceSize 和 malloc_size 的结果都为 16 个字节。
如果有Person和Student两种类型,Student 继承自 Person,各包含一个int类型的成员变量,那么其实例对象会占用多少内存?
// Person
@interface Person : NSObject
{
@public
int _age;
}
@property (nonatomic, assign) int height;
@end
@implementation Person
@end
// Student
@interface Student : Person
{
int _no;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
// => 16
NSLog(@"stu - %zd", class_getInstanceSize([Student class]));
// => 16
NSLog(@"stu - %zd", malloc_size((__bridge const void *)stu));
Person *person = [[Person alloc] init];
// => 16
NSLog(@"person - %zd", class_getInstanceSize([Person class]));
// => 16
NSLog(@"person - %zd", malloc_size((__bridge const void *)person));
}
return 0;
}
按说 Person 只有两个成员变量, isa 指针占8个字节, _age占4个字节,class_getInstanceSize([Person class]) 应该返回的是 12 个字节,但是结果返回的却是16个字节,这是因为有内存对齐:结构体的大小必须是最大成员大小的倍数。
如果 Student 有三个int类型的成员变量,那么其实例对象会占用多少内存?
@interface Student : NSObject
{
@public
int _no;
int _age;
int _gender;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
stu->_gender = 6;
// => 24
NSLog(@"%zd", class_getInstanceSize([Student class]));
// => 32
NSLog(@"%zd", malloc_size((__bridge const void *)stu));
}
return 0;
}
class_getInstanceSize 返回 24 我们可以预料到,但是 malloc_size 怎么会返回 32 呢?
- 结构体计算内存大小,存在内存对齐,按照 isa 8字节进行对齐
- 操作系统分配内存时候也存在内存对齐,iOS 在堆空间分配内存都是16字节的倍数
class_getInstanceSize([Student class]):至少需要24字节malloc_size((__bridge const void *)stu):实际分配32字节