iOS八股文(一)对象的本质探索(上)

1,780 阅读4分钟

对象的本质

Objective-C 代码的底层都是通过 C/C++ 实现,所以 Objective-C 面向对象是基于 C/C++ 数据结构实现。  下图为OC语言转换成机器语言的几个过程

9735490aae7a4eb68f2fc2945a3ee13e~tplv-k3u1fbpfcp-zoom-in-crop-mark-1304-0-0-0.image.png

可以通过clong编译器完成第一个过程。 把OC代码重写成C++代码。

clang -rewrite-objc <oc fileName> -o <c++ fileName>

也可以指定平台、指定指定架构模式,代码量少点,便于研究.

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc < OC FileName> -o <Cpp FileName>

image.png

OC代码

@interface OSTestObject1 : OSTestObject
{
    @public int _count2;
}
@end

@interface OSTestObject : NSObject
- (void)print;
@end

int object_c_source_m() {
    OSTestObject1 *obj1 = [[OSTestObject1 alloc] init];
    return 0;
}

C++代码(删减很多,方便阅读)

struct NSObject_IMPL {
    Class isa;
};

struct OSTestObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};

struct OSTestObject1_IMPL {
    struct OSTestObject_IMPL OSTestObject_IVARS;
    int _count2;
};

int object_c_source_m() {
    OSTestObject1 *obj1 = ((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("OSTestObject1"), sel_registerName("alloc")), sel_registerName("init"));
    return 0;
}

结论:

OC对象的本质其实是结构体,对象里面存储了对象的成员变量信息。

对象创建的流程

我们知道一个对象的init其实根本是掉用了c语言里面的calloc函数。但其实在这个操作之前还需要知道该对象所需要的内存大小。所以在objc4源码中可以看到(在objc4-818.2/runtime/objc-runtime-new.mm中的_class_createInstanceFromZone方法的实现)

image.png 这里就是对象创建的关键代码。我们可以大致总结为3个部分。

1.计算对象内存大小 (cls->instanceSize)
2.向系统请求内存空间 (calloc)
3.设置isa指针,关联到类对象,初始化对象信息 (obj->initInstanceIsa)

对象的大小

在OC中有3个关于对象大小的API。

API所属库含义
sizeoffoundation成员变量所占空间
class_getInstanceSizeruntime是所有成员变量所占空间
malloc_sizemalloc是对象分配的空间

其中 sizeofclass_getInstanceSize的区别是,sizeof为符号,在编译的过程就确定了值,而class_getInstanceSize为函数,在运行的过程中才知道结果。

上代码:

struct objcet_test {
    Class isa;
};

- (void)test1 {
    NSLog(@"结构体所占内存为%zd",sizeof(struct objcet_test));
    NSObject *objc1 = [[NSObject alloc] init];
    NSLog(@"nsobject 的成员变量所占空间为%zd",class_getInstanceSize([NSObject class]));
    NSLog(@"objc1所占内存空间为%zd",malloc_size((__bridge const void *)objc1));
}

运行结果:

2022-04-18 14:41:00.898227+0800 ObjectStudy[9601:189987] 结构体所占内存为8 2022-04-18 14:41:00.746690+0800 ObjectStudy[9394:185408] nsobject 的成员变量所占空间为8

2022-04-18 14:41:00.746737+0800 ObjectStudy[9394:185408] objc1所占内存空间为16

内存对齐

造成上面情况的根本原因是内存对齐。可以大致理解为这样存储虽然数据空间会增大,但访问数据的效率会更高,是一种牺牲空间换取时间的操作。

在OC中是经过了2次内存对齐,一种是结构体的内存对齐,一种是对象的内存对齐。

其中class_getInstanceSize获取到的是结构体内存对齐后的结果。 而malloc_size获取到的是对象内存对齐后的结果。

结构体内存对齐规则

  1. 数据成员对齐规则:结构(struct)的第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)。 

  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储)。

  3. 收尾工作:结构体的总大小,也就是sizeof的结果必须是其内部最大成员的整数倍,不足的要补齐。

对象内存对齐

oc对象内存对齐可以粗暴的理解为所需的内存必须是16的倍数。这是苹果系统分配内存处理的。为了进一步优化内存读取效率,内存在使用的时候是有bucket这样一个概念的。苹果会把内存分为多个bucket,其中bucket最小的单位是16个字节。这点可以在libmalloc源码中侧面证实。

image.png 最后可以顺便在源码中参考一下苹果类似内存对齐的源码,使用位移运算。

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

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