OC实例对象的本质

/ 0

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不同的操作系统,内存对齐倍数可能不同

参考资料:https://opensource.apple.com/tarballs/libmalloc/