OC类转换为C++结构体
OC 代码编译过程:OC -> C/C++ -> 汇编 -> 机器代码
利用 Xcode 自带的 LLVM
编译器前端工具 clang
,可以将 OC
代码转成 C++
代码:
clang -rewrite-objc main.m -o main.cpp
一般不会直接使用上面的命令,而是指定转行的平台和架构:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
还可以指定更多参数,比如支持 ARC
、指定运行时系统版本:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64.cpp
可以发现转换后的 C++ 代码中有和 NSObject 类对应的结构体:
@interface NSObject {
Class isa;
}
@end
// 转C++后
struct NSObject_IMPL {
Class isa;
};
以此证明,OC 类转行成 C++ 代码后是结构体。结构体名为 OC类名_IMPL
Class是一个类型别名
Class 是 struct objc_class *
的类型别名,所以 isa 是一个名为 objc_class
的结构体指针:
typedef struct objc_class *Class;
一个NSObject对象占用16字节
NSObject *obj = [[NSObject alloc] init];
因为 NSObject_IMPL
中只有一个 isa
指针变量,所以这个 isa
指针变量的内存地址,也是 NSObject
对象的内存地址,即是 obj
指针指向的内存地址。
// 获取obj指针所指向的内存的大小 >> 16
NSLog(@"%zd", malloc_size((__bridge const void *)(obj)));
// 获取NSObject实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
也就是 NSObject 对象占用了 16 字节,其中 8 字节用于存储 isa 指针变量,其余 8 字节空闲。
从 objc4
源码中也可以证明,所有 OC 对象最小分配内存为 16 字节:
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
参考资料:https://opensource.apple.com/tarballs/objc4/
一个字节占8位,16进制一个符号占4位(
16进制最大的符号 F 转二进制为 1111
),所以一个字节可以用2个16进制符号表示。
使用内存调试工具 Debug - Debug Workflow - View Memory
可以明显看出前 8 字节有数据,而后 8 字节没有数据。
或者使用 lldb
指令:
LLDB读写指令
读取内存:
memory read/[数量][格式][字节数] [内存地址]
x/[数量][格式][字节数] [内存地址]
x/3xw 0x10101010
格式:
x: 16进制
f: 浮点
d: 10进制
字节数:
b: byte 1字节
h: half word 2字节
w: word 4字节
g: giant word 8字节
修改内存:
memory write 内存地址 数值
memory write 0x10101010 10
大小端
CPU 大小端模式不同,从内存中读取数据的方式也不同,在 iOS 是小端模式。
大端:从低地址开始读取数据。
小端:从高地址开始读取数据。
验证
读取 _no
,4字节:
0x04 0x00 0x00 0x00 -> 0x00 00 00 04
读取 _age
,4字节:
0x05 0x00 0x00 0x00 -> 0x00 00 00 05
内存对齐
结构体占用的内存大小,必须是占用内存大小 最大的成员
所占内存大小的 倍数
。
下面的代码可以直观的看出来成员变量总共占 20
字节内存空间:
@interface Student : NSObject {
int _no;
int _age;
int _height;
}
@end
// 转C++
struct NSObject_IMPL {
Class isa;
};
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8,父类成员里只有一个指针变量
int _no; // 4
int _age; // 4
int _height; // 4
};
打印实例对象所占内存和最终为这个对象开辟的内存:
Student *student = [[Student alloc] init];
NSLog(@"%zd", class_getInstanceSize([Student class])); // 24
NSLog(@"%zd", malloc_size((__bridge const void *)student)); // 32
Student
对象的成员变量本身只占用了 20
字节,由于内存对齐原因导致 class_getInstanceSize
获取的大小为最大成员变量 struct NSObject_IMPL NSObject_IVARS;
的倍数,也就是 24
字节。
但是 malloc_size
获取的大小为 32
字节,这是因为向操作系统申请内存时,操作系统也会根据申请的内存大小进行内存对齐。
申请内存会调用 calloc
函数:
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
然后调用 _malloc_zone_calloc
函数,参数 size
为根据成员变量内存对齐后的 24
。这这个函数中根据 size
参数又进行了一次内存对齐:
MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start) {
internal_check();
}
ptr = zone->calloc(zone, num_items, size);
if (os_unlikely(malloc_logger)) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
if (os_unlikely(ptr == NULL)) {
malloc_set_errno_fast(mzo, ENOMEM);
}
return ptr;
}
除了计算结构体占用内存需要内存对齐,在调用 calloc
函数真正分配内存的时候,也是需要内存对齐。这两处的内存对齐不是一个概念,不能混为一谈。
在这个例子里,也就是经过结构体成员变量的内存对齐后,计算出需要 24
字节内存空间。然后调用 calloc
函数去申请内存,根据申请的内存大小,操作系统又进行一次内存对齐操作。
在 iOS 操作系统里,堆空间分配内存,都是 16
的倍数,所以这里返回了 32
。不同的操作系统,内存对齐倍数可能不同。