Objective-C · 2022年5月6日 0

iOS 内存对齐

前言

通过上篇文章 我们了解 alloc 底层最终调用的是 _class_createInstanceFromZone 方法开辟内存空间 下面就来分析一下该方法做了什么?

_class_createInstanceFromZone (已去掉多余代码)

static ALWAYS_INLINE id
_class_createInstanceFromZone()
{
    size_t size;
    size = cls->instanceSize(extraBytes);//计算内存大小
    if (outAllocatedSize) *outAllocatedSize = size;
    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);//开辟内存空间
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);//初始化 isa 指针和类关联
    } else {

        obj->initIsa(cls);
    }
//isa 指针
    if (fastpath(!hasCxxCtor)) {
        return obj;
    }
}

对于第一个 instanceSize() 方法

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

可以看到 在有缓存的时候 fastInstanceSize() 内部是16字节对齐的算法

size_t fastInstanceSize(size_t extra) const{

      return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);  
    }

而在首次初始化对象时调用 alignedInstanceSize() 方法 最终来到 word_align 方法 可以看出这是个8字节对齐算法

#define WORD_MASK 7UL // 64 位操作系统下代表7

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

测试一下

x = 6
计算 (6 + 7) & ~7
(1011) & ~ 0111
~0111 = 1000
相当于 (1011) & 1000
结果: 1000  结果为8字节
&~ 相当于: 右移 3 位 再左移 3 位

思考:有缓存的时候是 16字节对齐,而没有缓存的时候却是8 字节对齐,那实例对象究竟是 8字节对齐 还是16字节对齐呢?

带着这个问题 继续查看 _class_createInstanceFromZone 方法

if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);//开辟内存空间
    }

void *calloc(size_t __count, size_t __size)

  • count -- 要分配的元素个数
  • size -- 分配的元素大小

先看一个 calloc demo

#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
  @autoreleasepool {
    void *p1 = calloc(1, 10);// 分配一个长度为 10 的连续空间
    void *p2 = calloc(2, 20);// 分配两个长度为 20 的连续空间
    NSLog(@"%lu",malloc_size(p1));
    NSLog(@"%lu",malloc_size(p2));
   }
    return 0;
}

控制台输出结果:

2022-05-06 14:28:56.846074+0800 OCDemo[30323:94337] 16
2022-05-06 14:28:56.846980+0800 OCDemo[30323:94337] 48

通过上面的结果得出系统实际分配内存的时候是以16字节对齐的,对象的内部则是以 8 字节对齐

为什么要内存对齐?

内存是以字节为基本单位,cpu 在存取数据时,以 块 为单位,而不是以字节为单位存取,字节对齐后会减少 cpu 存取次数,以空间换时间 可以降低 cpu 开销,提高 cpu 访问速率

通过代码 打印 对象实际所占内存大小

@interface Student : NSObject
@property(nonatomic,copy)NSString *name;//8字节
@property(nonatomic,assign)int age;//4字节
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *stu = [Student new];
        stu.name = @"Terry";
        stu.age = 18;
        NSLog(@"%lu",malloc_size((__bridge const void *)stu));
   }
   return 0
}
2022-05-06 16:19:12.923648+0800 OCDemo[64446:197025] 32

上面的输出结果:32字节,8 + 4 + isa(8字节) = 20,16 字节对齐后为 32

对象本质

在 iOS 开发中 我们定义的对象、属性、方法在编译的时候做了什么,我们不得而知,通过 Clang 生成 cpp 文件(Swift 则是通过 SIL),可以帮助我们更好的去分析。

把目标文件编译成 c++ 文件
clang -rewrite-objc main.m

打开 main.cpp 文件 搜索 Student 类

#ifndef _REWRITER_typedef_Student
#define _REWRITER_typedef_Student
typedef struct objc_object Student;
typedef struct {} _objc_exc_Student;
#endif

extern "C" unsigned long OBJC_IVAR_$_Student$_name;
extern "C" unsigned long OBJC_IVAR_$_Student$_age;
struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    NSString *_name;
};


不难发现 对象的本质就是一个叫做 objc_object 的结构体。
搜索 NSObject_IMPL

struct NSObject_IMPL {
    Class isa;
};

objc_object 的结构体里面存储了:isa 指针 + 成员变量的值,
即 对象的本质是 isa 加 成员变量的值
然后跟着 _class_createInstanceFromZone 方法继续往下走

断点到这里后 通过控制台 po obj

断点到 8029 行代码
(lldb) po obj
0x0000000100a279a0

断点到 8044 行代码
(lldb) po obj
<Student: 0x100a279a0>

在 8029 行代码 obj 没有与 Student 关联
当 obj 调用 initIsa()方法后
运行到 8044 行代码 obj 与 Student 进行了关联

总结

iOS 为对象开辟内存的流程:

  • 通过 instanceSize() 先计算对象需要开辟的内存空间,对象内部是 8 字节对齐算法
  • 系统通过 calloc() 分配实际所需内存空间 16字节对齐算法
  • 最后调用 initIsa() 初始化 isa 指针 和 类进行关联
  • cpu 是昂贵的资源,通过字节对齐算法 以空间换时间,降低开销,当我们看到源码字节对齐算法的时候,也要思考其背后的原因。