OC 对象的本质

342 阅读9分钟

概述

我们撸的 Objective-C 代码,底层实现都是 C/C++ 代码。

Objective-C 的面向对象是基于 C\C++ 的数据结构实现的,由于 OC 对象可以存放各种类型的数据(intfloatNSString),因此 Objective-C 的对象是基于 C\C++ 的 结构体 数据结构实现的。

底层探究

新建项目(macOS - Command Line Tool)

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
}

使用编译器 Clang 将上述 main.m 文件编译成 iPhone 64 位架构下的 c++ 代码(.cpp 文件)。 编译指令: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

生成的 cpp 文件多达 3万多行,在 .cpp 文件最后面,可以看到熟悉的 main 函数:

在 .cpp 文件中可以找到结构体 NSObject_IMPL()

struct NSObject_IMPL {
	Class isa;
};

当然不转化成 cpp 文件,而是直接进入 NSObject 的头文件,可以看到其定义为:

NSObject 包含一个 Class 类型的 isa 成员变量。

而 Class 是一个结构体指针:

由此可知:结构体 NSObject_IMPL 的成员变量 isa 是一个指针,在 64 位架构下,一个指针占用 8 个字节长度,NSObject_IMPL 仅有一个指针类型的成员变量,且isa 的内存地址即为结构体的内存地址,按照这样的推理 NSObject_IMPL 内存也应该是 8 个字节。

模拟图如下:

继续在 main 函数中添加代码:

可以看到 NSObjc 对象,函数class_getInstanceSize返回的内存大小是8 字节,malloc 分配的内存大小是 16。

为什么两者得到的内存大小不同?

下载 objc4 源码查看,源码下载地址: opensource.apple.com/tarballs/

找到 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());
}

可以看到调用alignedInstanceSize函数,其注释为:返回成员变量所占据大小(内存对齐 <- 函数 word_align)。
所以 class_getInstanceSize([NSObject class]) 的真实含义是,NSObject实例对象的成员变量所占用的内存大小(内存对齐)。

继续探究 alloc 的底层内存大小分配
我们知道 alloc 方法最终会调用 allocWithZone,所以源码中找到方法 _objc_rootAllocWithZone,该方法中会调用 class_createInstance

进一步找到到_class_createInstanceFromZone方法

id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

该方法会调用_class_createInstanceFromZone函数,在函数中可以看到起内存分配会最终调用 calloc 函数,参数为 size,而 size是通过 cls->instanceSize(extraBytes) 取得的

在函数 instanceSize中,当小于 16个字节时,则返回16个字节,当然其注释也说的很清楚 “CF(CoreFoundation) 框架下,所有的对象至少16个字节”

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;
        return size;
    }

小结:
1、系统分配了 16 字节给 NSObject 对象(可以通过通过 malloc_size 函数获得);
2、但 NSObject 对象内部只使用了8个字节的空间(64bit环境下,可以通过 class_getInstanceSize 函数获得) 3、分配的16个字节,前8个字节存放 NSObjct 的 isa 指针。

针对 NSObjct 内存分配,换个姿势,再战一次:
利用 Xcode 工具调试:
断点调试,可以看到 objc 的地址为 “0x100607060”。

查看地址 0x100607060 开始的一段内存存储空间(“Debug -> Debug Workflow -> View Memory”)

可以基本印证分配的是 16 个字节,前面 8 个字节存放 isa,而后面 8 字节全为0。

如果 oc 对象添加其它成员变量,其内存占用多大? oc 对象的方法不占用内存吗?

含有成员变量的 对象

main.m 文件中添加 Person 类,Person 类中,添加成员变量 _height 和 _int:

@interface Person : NSObject
{
    @public
    int _height;
    int _age;
}

@end

@implementation Person

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}

同样将main.m 文件转换成 c++ 文件,同样类似在 cpp 文件中找到 Person 的底层结构体实现

struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _height;
	int _age;
};

Person_IMPL 的成员 struct NSObject_IMPL 就是上述 NSObject 的底层结构体实现且其中只有一个成员isa,因此 Person_IMPL 可以看成:

struct Person_IMPL {
    Class isa;
    int _height;
    int _age;
};

也就是说在继承情况下,子类的底层结构体实现中,可以理解为:
子类会继承父类的结构体成员,且父类的结构体成员会放在子类的结构体成员之前。

main 函数实例化 Person,并对成员变量赋值

        Person *p = [[Person alloc] init];
        p->_height = 60;
        p->_age = 28;

结合 Person 的底层结构体实现,上述代码可以理解为:分配一段存储空间给 Person 对象,并以此存放 isa、_height 和 _age,分别占 8字节、4字节、4字节且三个成员地址连续。并用 p指针指向这个 Person 对象,p 的地址值即为 isa 的地址。

至于分配多大内存空间给 Person,同样可以添加相关 NSLog 代码:

即 Person 在内存中所占据的存储空间是:16 字节。

另外上面提到 Person 的本质其实是 Person_IMPL,那么可以将 p强制转化成 Person_IMPL 类型结构体:

可以印证:Person 对应的 c++ 底层结构体实现即为:Person_IMPL。

再换个姿势,同样通过断点查看内存:

这里顺带提一下,因为 iOS 是小端模式,所以中间四字节(_height)的十六进制是:0x0000003C,即十进制的60,_age同理。

使用 LLDB 指令调试:也可以得到内存数据信息

尝试通过修改内存地址进而修改 _height 的值:

上述内存地址分析正确无误。

小结: Person 对象的本质为:

继承关系的 OC 对象

Male 中一个成员变量 _height,继承Person; Person 中一个成员变量_age, 继承 NSObject:

@interface Person : NSObject
{
    int _height;
}

@end

@implementation Person

@end



@interface Male : Person
{
    int _age;
}

@end

@implementation Male

@end

将 main.m 编译成 cpp 文件,并依次找到 Person 和 Male 的底层结构体实现:

struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _height;
};

struct Male_IMPL {
	struct Person_IMPL Person_IVARS;
	int _age;
};

Person_IMPL 根据上面提到的 size_t instanceSize(size_t extraBytes) 函数或者内存对齐原则(结构体的大小必须是最大成员大小的倍数),可以得到其内存大小为:16 字节。

对于 Male_IMPL 第一个结构体成员 Person_IMPL 会分配 16字节,_age 4字节,16 + 4 = 20,根据结构体内存对齐原则好像应该分配 24 字节。然而并非如此,而是分配 16 字节。struct Person_IMPL Person_IVARS 分配 16字节,其中前8字节用于存放 isa,紧跟的4字节用于存放 _height,最后面还有4字节空出来,刚好可以被利用放下 _age (4字节)。 代码验证:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person = [[Person alloc] init];
        NSLog(@"person - instanceSize: %zd", class_getInstanceSize([Person class]));
        NSLog(@"person - malloc_size: %zd",malloc_size((__bridge const void *)(person)));
        
        Male *male = [[Male alloc] init];
        NSLog(@"male - instanceSize: %zd", class_getInstanceSize([Male class]));
        NSLog(@"male - malloc_size: %zd",malloc_size((__bridge const void *)(male)));
    }
    return 0;
}

输出:

OC 对象本质_Test02[80050:1932487] person - instanceSize: 16
OC 对象本质_Test02[80050:1932487] person - malloc_size: 16
OC 对象本质_Test02[80050:1932487] male - instanceSize: 16
OC 对象本质_Test02[80050:1932487] male - malloc_size: 16

注意:class_getInstanceSize(Class _Nullable cls) 返回的是经过内存对齐的成员变量的大小,所以 Preson 返回的是 16 而非 12

多成员变量的 OC 对象内存大小

Person 对象中添加3个成员变量

// Person 类
@interface Person : NSObject
{
    int _height;
    int _age;
    int _weight;
}

@end

@implementation Person

@end

编译成 cpp ,Person_IMPL:

// NSObject 对象的底层结构体实现(.cpp 文件中)
struct NSObject_IMPL {
    Class isa;
};

// Person 对象的底层结构体实现(.cpp 文件中)
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _height;
    int _age;
    int _weight;
    
};

在 main 函数中添加测试代码:

// main 函数
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person = [[Person alloc] init];
        NSLog(@"Person_IMPL - sizeof: %zd", sizeof(struct Person_IMPL));
        NSLog(@"person - instanceSize: %zd", class_getInstanceSize([Person class]));
        NSLog(@"person - malloc_size: %zd",malloc_size((__bridge const void *)(person)));
    }
    return 0;
}

输出 log:

OC 对象本质_Test02[80415:1953835] Person_IMPL - sizeof: 24
OC 对象本质_Test02[80415:1953835] person - instanceSize: 24
OC 对象本质_Test02[80415:1953835] person - malloc_size: 32

Person 底层结构的是Person_IMPL类型,从日志,可以看出:
1、sizeof(struct Person_IMPL) 得到结构体大小,根据结构体内存对齐原则,24 字节;
2、instanceSize 取的 Person 对象成员变量经过内存对齐的内存大小,也是 24 字节,而且按照上面的分析,24 字节够用也符合结构体内存对齐;
3、然而 malloc_size 却分配了 32 字节的内存。

class_getInstanceSize() 从源码分析:
objc4 源码下载地址: opensource.apple.com/tarballs/ob…

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());
}

其中uint32_t alignedInstanceSize()的注释写的很清楚 - 类的成员变量的大小(内存对齐),8(isa) + 4(_height) + 4(_age) + 4(_weight) = 20,内存对齐会分配 24 字节。

alloc 的 size 分析 上面在研究 alloc 的底层大小分配的时候,我们知道最终会来到函数 _class_createInstanceFromZone 并在该函数中调用 calloc,calloc 的入参 size 是通过 size_t instanceSize(size_t extraBytes) 中的 size_t size = alignedInstanceSize() + extraBytes; 取得的:

其中 alignedInstanceSize() 为内存对齐后的成员变量的内存大小。而extraBytes 又是怎么来的、其值是多少呢?根据函数调用,可以找到最初传参时的函数:

参数值为 0,所以其实 size_t size = alignedInstanceSize() + extraBytes 相当 于size_t size = alignedInstanceSize() + 0,即 size 大小为对象内存对齐后的成员变量的内存大小。

就 Person 对象而言 class_getInstanceSize(Class _Nullable cls) 的底层调用:

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

最终又来到了返回内存对齐的成员变量的大小的函数,得到 24 字节:

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
}

也就是在 calloc 之前的 size 其实都是 24 字节,但为什么最终 malloc 得到的确是 32 字节?
通过 libmalloc 源码,找到 malloc.c 文件。
libmalloc 源码下载地址: opensource.apple.com/tarballs/li…

在 malloc.c 中可以找 calloc 的函数实现:

void *
calloc(size_t num_items, size_t size)
{
	void *retval;
	retval = malloc_zone_calloc(default_zone, num_items, size);
	if (retval == NULL) {
		errno = ENOMEM;
	}
	return retval;
}

该函数内部调用 malloc_zone_calloc()

其中 ptr = zone->calloc(zone, num_items, size) 中的 zone->calloc 的中 zone 为上一函数的入参:default_zone,其定义为:

static malloc_zone_t *default_zone = &virtual_default_zone.malloc_zone;

所以进入 default_zone_calloc 函数:

依次进行下面过程的函数流程:

最后来到segregated_size_to_fit

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
	size_t k, slot_bytes;

	if (0 == size) {
		size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
	}
	k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
	slot_bytes = k << SHIFT_NANO_QUANTUM;							// multiply by power of two quanta size
	*pKey = k - 1;													// Zero-based!

	return slot_bytes;
}

在函数内部会经过右移和左移运算。
以上述 Person 对象的状况为例:size = 24(结构体对齐时传参 calloc 的值) 上述运算过程中有两个宏定义:

#define SHIFT_NANO_QUANTUM		4
#define NANO_REGIME_QUANTA_SIZE		(1 << SHIFT_NANO_QUANTUM)	// 16

运算过程:
1、右移运算:(24 + 16 -1) >> 4,即 0000 0000 0010 0111 >> 4 = 0000 0000 0000 0010 ;
2、左移4位运算: 2 << 4,即 0000 0000 0000 0010 << 4 = 32。

可以看到 OC 对象自身的内存对齐,是按照 16 字节进行对齐的,所以 Person 对象 malloc 最终返回 32 字节。如果不满 16 字节按照 16 字节,这也证明了最开始 NSObject 的 malloc_size 返回的 16。

关于 iOS 内存对齐原则总结如下:
1、对象的成员是按照 8 字节对齐;
2、对象本身是按照 16 字节对齐。 calloc <- alloc

上面 class_getInstanceSize - 函数,可理解为返回对象成员变量的所需要的内存大小 - 8 字节对齐原则;
malloc - 函数为对象本身分配的内存大小 - 16 字节对齐;
sizeof - 运算符,取数据类型大小。