每日分享 – ios底层原理

获取内存大小的三种方式

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

class_getInstanceSize

这个方法在底层 2中就已经介绍过了,是 runtime 提供的

API,用于获取类的实例对象所占用的内存大小,并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小

mallocsize

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

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

#import <Foundation/Foundation.h>

#import <objc/runtime.h>

#import <malloc/malloc.h>

#import "SATest.h"

#import <objc/runtime.h>

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        // insert code here...

        NSLog(@"Hello, World!");        

        NSObject *obj = [NSObject alloc];

        NSLog(@"%lu",sizeof(obj));

        NSLog(@"%zu",class_getInstanceSize([obj class]));

        NSLog(@"%zu",malloc_size((__bridge const void*)(obj)));

    }

    return 0;

}

以下是打印结果

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 字节对齐算法来解释这个问题

结构体内存对齐

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

struct MyStruct1{

    char a; //1   [0]

    double b;//8  [8,9,10,11,12,13,14,15]

    int c;//4     [16,17,18,19]

    short d;//2   [20,21]

}MyStruct1;

struct MyStruct2{

    char a;   //[14]

    double b; //[0,1,2,3,4,5,6,7]

    int c;    //[8,9,10,11]

    short d;  //[12,13]

}MyStruct2;

        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.如下所示

//1、结构体嵌套结构体

struct Mystruct3{

    double b;   //8字节

    int c;      //4字节

    short d;    //2字节

    char a;     //1字节

    struct Mystruct2 str; 

}Mystruct3;

//2、打印 Mystruct3 的内存大小

NSLog(@"Mystruct3内存大小:%lu", sizeof(Mystruct3));

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

二次验证

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

struct Mystruct4{

    int a;              //4字节 min(0,4)--- (0,1,2,3)

    struct Mystruct5{   //从4开始,存储开始位置必须是最大的整数倍(最大成员为8),min(4,8)不符合 4,5,6,7,8 -- min(8,8)满足,从8开始存储

        double b;       //8字节 min(8,8)  --- (8,9,10,11,12,13,14,15)

        short c;         //2字节,从16开始,min(16,2) -- (16,17)

    }Mystruct5;

}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,并定义属性
@interface YXPerson : NSObject

@property(nonatomic,copy)NSString * name;

@property(nonatomic,copy)NSString * nickName;

@property(nonatomic,assign)int age;

@property(nonatomic)char c1;

@property(nonatomic)char c2;

@end

  • 在 main 中创建 YXPerson 对象,并给属性赋值
int main(int argc, const char * argv[]) {

    @autoreleasepool {

        // Setup code that might create autoreleased objects goes here.

        YXPerson *person = [[YXPerson alloc] init];

        person.name = @"YX";

        person.nickName = @"XC";

        person.age = 18;

        person.c1 = "a";

        person.c2 = "b";

        NSLog(@"");

    }

    return NSApplicationMain(argc, argv);

}

  • 断点调试 person,根据 person 的地址,找出属性的值
    • 通过地址找出 name&nickName 的值

image.png

* 当我们想通过地址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进行分析

size_t class_getInstanceSize(Class cls)

{

    if (!cls) return 0;

    return cls->alignedInstanceSize();

}

    uint32_t alignedInstanceSize() const {

        return word_align(unalignedInstanceSize());

    }

static inline uint32_t word_align(uint32_t x) {

    return (x + WORD_MASK) & ~WORD_MASK;

}

#   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 字节对齐算法

static inline size_t align16(size_t x) {

    return (x + size_t(15)) & ~size_t(15);

}

segregated_size_to_fit:16 字节对齐算法

#define SHIFT_NANO_QUANTUM      4

#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16

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;

}

算法原理:

算法原理:k + 15 >> 4 << 4 ,其中 右移4 + 左移4相当于将后4位抹零,跟 k/16 * 16一样 ,是16字节对齐算法,小于16就成0了

正文完