概述
我们撸的 Objective-C
代码,底层实现都是 C/C++
代码。
Objective-C
的面向对象是基于 C\C++
的数据结构实现的,由于 OC
对象可以存放各种类型的数据(int
、float
、NSString
),因此 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 - 运算符,取数据类型大小。