OC底层探究 - OC实例对象的本质

328 阅读7分钟

OC实例对象的本质

  • 我们平时编写的OC代码,底层实现其实都是C/C++代码,所以OC的面向对象都是基于C/C++的数据结构实现的,比如:OC里的对象、类主要是基于C/C++的结构体 Struct 数据结构实现的。

oc本质

  • 将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内存地址

可以看到,obj 在内存中的地址,根据这个地址,我们可以看到该实例对象在内存中的具体情况。

通过Xcode Debug工具

Debug -> Debug Workfllow -> View Memory (Shift + Command + M)

View Memory

之后根据 obj 的地址,我们可以查看到该对象在内存中的具体存储情况:

内存分布

一个16进制位代表4个二进制位,两个16进制位就代表8个二进制位,也就是一个字节。可以看到,自目标地址往后的16个字节中存储的就是 obj 对象,前八个字节里存储了数据,后八个字节为空。

通过LLDB指令

常用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 个字节。

Student

如果有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字节