前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >IOS 内存对齐原理

IOS 内存对齐原理

原创
作者头像
ruochen
发布2021-12-15 14:32:30
1.4K0
发布2021-12-15 14:32:30
举报

获取内存大小的三种方式

  • sizeof
  • class_getInstanceSize
  • malloc_size
  • sizeof是一个操作符,不是函数
  • 我们一般用 sizeof 计算内存大小时,传入的对象主要是数据类型,这个在编译器的编译阶段(即编译时)就会确定大小,而不是在运行时
  • sizeof最终得到的结果是该数据类型占用空间的大小

mallocsize

这个函数是获取系统实际分配内存的大小

可以通过以下的代码输出,验证我们上面的说法

代码语言:txt
复制
#import <Foundation/Foundation.h>
代码语言:txt
复制
#import <objc/runtime.h>
代码语言:txt
复制
#import <malloc/malloc.h>
代码语言:txt
复制
#import "SATest.h"
代码语言:txt
复制
#import <objc/runtime.h>
代码语言:txt
复制
int main(int argc, const char * argv[]) {
代码语言:txt
复制
    @autoreleasepool {
代码语言:txt
复制
        // insert code here...
代码语言:txt
复制
        NSLog(@"Hello, World!");        
代码语言:txt
复制
        NSObject *obj = [NSObject alloc];
代码语言:txt
复制
        NSLog(@"%lu",sizeof(obj));
代码语言:txt
复制
        NSLog(@"%zu",class_getInstanceSize([obj class]));
代码语言:txt
复制
        NSLog(@"%zu",malloc_size((__bridge const void*)(obj)));
代码语言:txt
复制
    }
代码语言:txt
复制
    return 0;
代码语言:txt
复制
}

以下是打印结果

image.png

总结

  • sizeof:计算类型占用的内存大小,其中可以放基本数据类型,对象,指针
    • 对于 int 这类的基本数据类型而言,sizeof 获取的就是数据类型占用的内存大小,不同的数据类型占用的内存是不一样的
    • 而对于 NSObject定义的实例对象而言,其对象类型的本质就是一个结构体(即struct objc_object)的指针,所以sizeof(obj)打印的是对象obj 指针的大小,我们知道一个指针的大小是 8,所以 sizeof 打印的是 8, 注意:这里的 8 字节和 isa 指针一点关系都没有
    • 对于指针而言,sizeof 打印的结果就是 8,因为一个指针的内存大小就是 8
  • class_getInstanceSize:计算对象实际占用内存的大小,这个需要依据类的属性而变化,如果自定义类没有自定义属性,仅仅只是继承自 NSObject,则类的实例对象实际占用的内存大小是8,可以简单的理解为 8 字节对齐
  • mallocsize:计算对象实际分配内存大小,这个是由系统完成的,可以从上面的打印结果看出,实际分配的和实际占用的内存并不相等,这个可以根据底层 2中的16 字节对齐算法来解释这个问题

结构体内存对齐

接下来我们首先定义两个结构体,分别计算他们的内存大小,来引入今天的主体,内存对齐原理

代码语言:txt
复制
struct MyStruct1{
代码语言:txt
复制
    char a; //1   [0]
代码语言:txt
复制
    double b;//8  [8,9,10,11,12,13,14,15]
代码语言:txt
复制
    int c;//4     [16,17,18,19]
代码语言:txt
复制
    short d;//2   [20,21]
代码语言:txt
复制
}MyStruct1;
代码语言:txt
复制
struct MyStruct2{
代码语言:txt
复制
    char a;   //[14]
代码语言:txt
复制
    double b; //[0,1,2,3,4,5,6,7]
代码语言:txt
复制
    int c;    //[8,9,10,11]
代码语言:txt
复制
    short d;  //[12,13]
代码语言:txt
复制
}MyStruct2;
代码语言:txt
复制
        NSLog(@"结构体1- %lu   结构体2- %lu",sizeof(MyStruct1),sizeof(MyStruct2));

打印结果如下

image.png

从打印结果可以看出一个问题,两个结构体看起来没什么区别,唯一的区别就是其中的变量顺序不一致,导致他们所占用内存大小不相等,这就是ios 中内存字节对齐现象

内存对齐规则

每个特定平台上的编译器都有自己的默认"对齐系数",程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16 来改变这一系数,其中n

就是对齐系数,在 ios 中,xcode 默认是#pragma pack(8),即 8 字节对齐

内存对齐原则主要分为以下三点

  • 原则1: 数据成员的对齐规则可以理解为 min(m,n)的公式,其中 m 表示当前成员的开始位置,n 表示当前成员所需的位数,如果满足条件m 整除 n(即 m%n == 0),n 从 m 位置开始存储,反之继续检查 m+1能否整除 n,直到可以整除,从而确定了成员的开始位置
  • 原则2:当数据成员为结构体时,作为数据成员的结构体的内部最大成员的大小作为该结构体的大小进行计算,比如结构体 a 嵌套结构体 b,b 中有 char int double,则 b 的自身长度为 8
  • 原则3: 最后结构体的内存大小必须是结构体中最大成员内存大小的整数倍,不足的需要补齐

验证对齐规则

下表是各种数据类型

image.png

我们可以通过下图来说明为什么两个结构体MyStruct1和 MyStruct2 内存大小为什么不一致的情况

image.png

MyStruct1内存大小计算

  • 变量 a,占一个字节,从 0 开始,此时min(0,1),第 0 位存储 a
  • 变量 b,占 8 个字节,min(1,8),不能被整除,向后推, 8-15 位放 b
  • 变量 c,占 4 个字节,min(16,4),16-19 位放 c
  • 变量 d,占两个字节,min(20,2), 20-21 位放 dundefined因此MyStruct1 需要的内存大小为 21,而其中最大变量为 8 字节,根据内存对齐原则,MyStruct1 的内存大小必须是 8 的倍数,向上取整到 24,所以 sizeof 的结果是 24

MyStruct2 内存大小计算

  • 变量b,占 8 个字节,从 0 开始,此时 min(0,8),0-7 位存储 b
  • 变量c,占 4 字节,min(8,4), 8-11 位存储 c
  • 变量d,占2字节,min(12,2),12-13 位存储 d
  • 变量a,占1字节,min(14,1),14 位存储 aundefined因此 struct2 需要的内存大小为 15,其中最大成员的字节数为 8,所以 struct2 的内存大小必须是 8 的倍数,向上取整到 16,所以 sizeof 的结果为 16

结构体嵌套结构体

首先定义一个MyStruct3,其中嵌套MyStruct2.如下所示

代码语言:txt
复制
//1、结构体嵌套结构体
代码语言:txt
复制
struct Mystruct3{
代码语言:txt
复制
    double b;   //8字节
代码语言:txt
复制
    int c;      //4字节
代码语言:txt
复制
    short d;    //2字节
代码语言:txt
复制
    char a;     //1字节
代码语言:txt
复制
    struct Mystruct2 str; 
代码语言:txt
复制
}Mystruct3;
代码语言:txt
复制
//2、打印 Mystruct3 的内存大小
代码语言:txt
复制
NSLog(@"Mystruct3内存大小:%lu", sizeof(Mystruct3));
代码语言:txt
复制
NSLog(@"Mystruct3中结构体成员内存大小:%lu", sizeof(Mystruct3.str));

打印结果如下

image.png

  • 分析 MyStruct3 的内存计算 变量b,占 8 个字节,从 0 开始,此时 min(0,8),0-7 位存储 b 变量c,占 4 字节,min(8,4), 8-11 位存储 c 变量d,占2字节,min(12,2),12-13 位存储 d 变量a,占1字节,min(14,1),14 位存储 a * 结构体成员 str,是一个结构体,根据内存对齐原则,结构体成员要从其内存最大成 员的整数倍开始存储,而 MyStruct2 的最大成员为 8,所以 str 要从 8 的整数倍开始存储,也就是从 16 开始存储, 16-31 存储 strundefined因此 MyStruct3 需要的内存大小为 32,而 MyStruct3 中的最大变量为 str,其内部最大成员为 8,所以 MyStruct3 的内存必须是 8 的倍数,所以sizeof 的结果是 32undefined其内存存储情况如下

image.png

二次验证

再次计算一个结构体,验证内存大小

代码语言:txt
复制
struct Mystruct4{
代码语言:txt
复制
    int a;              //4字节 min(0,4)--- (0,1,2,3)
代码语言:txt
复制
    struct Mystruct5{   //从4开始,存储开始位置必须是最大的整数倍(最大成员为8),min(4,8)不符合 4,5,6,7,8 -- min(8,8)满足,从8开始存储
代码语言:txt
复制
        double b;       //8字节 min(8,8)  --- (8,9,10,11,12,13,14,15)
代码语言:txt
复制
        short c;         //2字节,从16开始,min(16,2) -- (16,17)
代码语言:txt
复制
    }Mystruct5;
代码语言:txt
复制
}Mystruct4;

分析如下

  • 变量a,占 4 个字节, 0-3 存储a
  • 变量 MyStruct5,其内部最大成员为8,所以要从 8 的整数倍开始存储,也就是从 8 开始存储 变量 b,占 8 个字节,从 8 开始存储 min(8,8), 8-15 存储 b 变量 c,占1个字节,min(16,2),16-17 存储 cundefined因此 MyStruct4 需要的内存大小为 18, 根据内存对齐原则,内存大小必须是最大成员的整数倍, 其中最大成员为 8, 向上取整,所以 sizeof 最后的结果为 24

内存优化,属性重排

MyStruct1通过内存字节对齐原则,增加了 9 个字节,而 Mystruct2只增加了一个字节,结构体内存大小与结构体内的成员顺序有关

举个例子说明属性重排,也就是内存优化

  • 定义一个自定义的类YXPerson,并定义属性
代码语言:txt
复制
@interface YXPerson : NSObject
代码语言:txt
复制
@property(nonatomic,copy)NSString * name;
代码语言:txt
复制
@property(nonatomic,copy)NSString * nickName;
代码语言:txt
复制
@property(nonatomic,assign)int age;
代码语言:txt
复制
@property(nonatomic)char c1;
代码语言:txt
复制
@property(nonatomic)char c2;
代码语言:txt
复制
@end
  • 在 main 中创建 YXPerson 对象,并给属性赋值
代码语言:txt
复制
int main(int argc, const char * argv[]) {
代码语言:txt
复制
    @autoreleasepool {
代码语言:txt
复制
        // Setup code that might create autoreleased objects goes here.
代码语言:txt
复制
        YXPerson *person = [[YXPerson alloc] init];
代码语言:txt
复制
        person.name = @"YX";
代码语言:txt
复制
        person.nickName = @"XC";
代码语言:txt
复制
        person.age = 18;
代码语言:txt
复制
        person.c1 = "a";
代码语言:txt
复制
        person.c2 = "b";
代码语言:txt
复制
        NSLog(@"");
代码语言:txt
复制
    }
代码语言:txt
复制
    return NSApplicationMain(argc, argv);
代码语言:txt
复制
}
  • 断点调试 person,根据 person 的地址,找出属性的值
    • 通过地址找出 name&nickName 的值

image.png

代码语言:txt
复制
* 当我们想通过地址0x000000120000aba9找出 age,c1,c2 的值时,发现是乱码,这是因为苹果针对 age&c1&c2 的属性内存进行了重排,age 占 4 字节,c1 和 c2 各占一个字节, 所以他们三个存储在同一块内存中age 的获取通过0x00000012c1 的获取通过0x61(a的 ASCII码是 97)c2 的获取通过0x62(b 的ASCII码是98)

image.png

下图是 person 内存分布情况

person内存结构.png

注意undefined char 类型的数据读取出来是以ASCII 码的形式显示

总结

这里总结下苹果的内存对齐思想

  • 大部分内存都是通过固定的内存块进行读取
  • 尽管我们在内存中采用了内存对齐的方式,但是并不是所有内存都可以进行浪费的,苹果会自动对属性进行重排,用此来优化内存

字节对齐到底采用多少字节对齐

前面我们提到了 8 字节对齐,也提到了 16 字节对齐,我们到底是按照哪种进行对齐的呢

我们可以通过 objc 源码中的class_getInstanceSize进行分析

代码语言:txt
复制
size_t class_getInstanceSize(Class cls)
代码语言:txt
复制
{
代码语言:txt
复制
    if (!cls) return 0;
代码语言:txt
复制
    return cls->alignedInstanceSize();
代码语言:txt
复制
}
代码语言:txt
复制
    uint32_t alignedInstanceSize() const {
代码语言:txt
复制
        return word_align(unalignedInstanceSize());
代码语言:txt
复制
    }
代码语言:txt
复制
static inline uint32_t word_align(uint32_t x) {
代码语言:txt
复制
    return (x + WORD_MASK) & ~WORD_MASK;
代码语言:txt
复制
}
代码语言:txt
复制
#   define WORD_MASK 7UL
  • 通过上面的源码可知,对象真正的对齐方式是 8 字节对齐,8 字节对齐已经足够满足对象的需求了
  • apple 系统为了防止一切的容错,采用的是 16 字节对齐,主要是因为采用 8 字节对齐时,两个对象的内存会紧挨着,

总结

综合前文提到的获取内存大小的方式,

  • class_getInstanceSize:采用的是 8 字节对齐,参照对象属性内存大小
  • malloc_size:采用 16 字节对齐,参照整个对象的内存大小,对象实际分配内存的大小必须是 16 的倍数

内存对齐算法

目前已知的16 字节内存对齐算法有两种

  • alloc 源码分析中的align16
  • malloc 源码分析中segregated_size_to_fit

align16 16 字节对齐算法

代码语言:txt
复制
static inline size_t align16(size_t x) {
代码语言:txt
复制
    return (x + size_t(15)) & ~size_t(15);
代码语言:txt
复制
}

segregated_size_to_fit:16 字节对齐算法

代码语言:txt
复制
#define SHIFT_NANO_QUANTUM      4
代码语言:txt
复制
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16
代码语言:txt
复制
static MALLOC_INLINE size_t
代码语言:txt
复制
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
代码语言:txt
复制
{
代码语言:txt
复制
    size_t k, slot_bytes;
代码语言:txt
复制
    if (0 == size) {
代码语言:txt
复制
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
代码语言:txt
复制
    }
代码语言:txt
复制
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
代码语言:txt
复制
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
代码语言:txt
复制
    *pKey = k - 1;                                                  // Zero-based!
代码语言:txt
复制
    return slot_bytes;
代码语言:txt
复制
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 获取内存大小的三种方式
    • mallocsize
      • 总结
        • 结构体内存对齐
          • 内存对齐规则
            • 验证对齐规则
              • MyStruct1内存大小计算
                • MyStruct2 内存大小计算
                  • 结构体嵌套结构体
                    • 二次验证
                      • 内存优化,属性重排
                      • 总结
                        • 字节对齐到底采用多少字节对齐
                          • 总结
                          • 内存对齐算法
                            • align16 16 字节对齐算法
                              • segregated_size_to_fit:16 字节对齐算法
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档