OC底层原理04 - 内存对齐

获取内存大小的三种方式

  • sizeof
  • class_getInstanceSize
  • malloc_size

sizeof

sizeof是一个操作符,不是函数,一把用于计算内存大小。传入的主要对象是数据类型(基本数据类型、对象、指针),这个在编译器的编译阶段就会确定大小而不是在运行时。sizeof最终得到的结果是该数据类型占用空间的大小

class_getInstanceSize

这个方法在OC底层原理02 - alloc & init & new 源码分析分析时就已经分析了,是runtime提供的api,用于获取类的实例对象所占用的内存大小,并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小

malloc_size

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

我们通过运行以下代码验证上述所说

#import 
#import 
#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
        NSLog(@"objc对象类型占用的内存大小:%lu", sizeof(objc));
        NSLog(@"objc对象实际占用的内存大小:%lu", class_getInstanceSize([objc class]));
        NSLog(@"objc对象实际分配的内存大小:%lu", malloc_size((__bridge const void*)(objc)));
    }
    return 0;
}

运行结果如下
image.png

总结

sizeof

  • 计算类型占用的内存大小,其中可以放基本数据类型对象指针
  • 对于类似于int这样的基本数据而言,sizeof获取的就是数据类型占用的内存大小,不同的数据类型所占用的内存大小是不一样的
  • 而对于类似于NSObject定义的实例对象而言,其对象类型的本质就是一个结构体(即 struct objc_object)的指针,所以sizeof(objc)打印的是对象objc的指针大小,我们知道一个指针的内存大小是8,所以sizeof(objc) 打印是 8
  • 对于指针而言,sizeof打印的就是本身的内存大小8

class_getInstanceSize

计算对象实际占用的内存大小,这个需要依据类的属性而变化,如果自定义类没有自定义属性,仅仅只是继承自NSObject,则类的实例对象实际占用的内存大小是8

malloc_size

计算对象实际分配的内存大小,这个是由系统完成的,可以从上面的打印结果看出,实际分配的和实际占用的内存大小并不相等,这个问题可以通过OC底层原理02 - alloc & init & new 源码分析中的16字节对齐算法来解释这个问题

结构体内存对齐

首先我们定义两个结构体,分别计算他们的内存大小。

struct HLStruct1 {
    double a;
    char b;
    int c;
    short d;
}struct1;

struct HLStruct2 {
    double a;
    int b;
    char c;
    short d;
}struct2;

//计算 结构体占用的内存大小
NSLog(@"%lu-%lu", sizeof(HLstruct1), sizeof(HLstruct2));

输出结果如下

image.png
从打印结果我们可以看出,两个结构体定义的变量变量类型都是一致的,唯一的不同是定义变量的顺序不一致,但是他们所占用的内存大小不相等却不相同。这就是iOS中的内存字节对齐

内存对齐规则

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n = 1, 2, 4, 8, 16来改变这一系数,其中的n就是你要指定的“对齐系数”。在iOS中,Xcode默认为#pragma pack(8),即8字节对齐

内存字节对齐原则

数据成员对齐规则:struct 或者 union 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数据、结构体等)的整数倍开始(例如int在32位机中是4字节,则要从4的整数倍地址开始存储)
数据成员为结构体:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储)
结构体的整体对齐规则:结构体的总大小,即sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐

分析

下表是各数据类型在C和OC中所占内存大小

image.png
根据内存对齐原则以及各数据类型所占内存大小,画出上述两个结构体HLStruct1HLStruct2的内存结构示意图
image.png

计算过程详解:
HLStruct1:

  • 变量a:占8个字节,从0开始,即0-7字节存储变量a
  • 变量b:占1个字节,从8开始,即8字节储存变量b
  • 变量c:占4个字节,9不能整除4,故往后顺移,直到12能整除4,所以从12开始,即12-15字节储存变量c
  • 变量d:占2个字节,从16开始,即16-17字节储存变量d
    因此HLStruct1的需要的内存大小为18字节,而HLStruct1最大变量的字节数为8HLStruct1实际的内存大小必须是8的整数倍18向上取整到24,所以sizeof(HLStruct1)的结果是 24
    HLStruct2:
  • 变量a:占8个字节,从0开始,即0-7字节存储变量a
  • 变量b:占4个字节,从8开始,即8-11字节储存变量b
  • 变量c:占1个字节,从12开始,即12字节储存变量c
  • 变量d:占2个字节,13不能整除2,故往后顺移,直到14能整除2,所以从14开始,即14-15字节储存变量d
    因此HLStruct2的需要的内存大小为16字节,而HLStruct2最大变量的字节数为8HLStruct2实际的内存大小必须是8的整数倍16刚好为8的倍数,所以sizeof(HLStruct2)的结果是 16

结构体嵌套结构体

上面的两个结构体只是简单的定义数据成员,下面来一个比较复杂的,结构体中嵌套结构体的内存大小计算情况
定义一个结构体HLStruct3,在HLStruct3中嵌套HLStruct2,如下所示

struct HLStruct3 {
    double a;
    int b;
    char c;
    short d;
    int e;
    struct HLStruct2 f;
}struct3;

输出

NSLog(@"%lu-%lu", sizeof(struct3), sizeof(struct3.f));

输出结果如下

image.png

struct3内存计算

  • 变量a:占8个字节,从0开始,即0-7字节存储变量a
  • 变量b:占4个字节,从8开始,即8-11字节储存变量b
  • 变量c:占1个字节,从12开始,即12字节储存变量c
  • 变量d:占2个字节,13不能整除2,故往后顺移,直到14能整除2,所以从14开始,即14-15字节储存变量d
  • 变量e:占4个字节,从16开始,即16-19字节储存变量e
  • 变量f:占16个字节,f是一个结构体,根据内存对齐原则二,结构体成员要从其内部最大成员大小的整数倍开始存储,而HLStruct2中最大的成员大小为8,所以f要从8的整数倍开始,当前是从20开始,所以不符合要求,需要往后移动到24,24是8的整数倍,符合内存对齐原则,即24-39存储变量f
    因此HLStruct3的需要的内存大小为40字节,而HLStruct3最大变量的字节数为8HLStruct3实际的内存大小必须是8的整数倍40刚好为8的倍数,所以sizeof(HLStruct3)的结果是 40
    image.png

二次验证

在定义一个结构体,如下所示

struct HLStruct4 {
    short a;
    double b;
}struct4;
struct HLStruct5 {
    char a;
    int b;
    struct HLStruct4 c;
}struct5;

HLStruct4内存计算

  • 变量a:占2个字节,从0开始,即0-2字节存储变量a
  • 变量b:占8个字节,3不能整除8,故往后顺移,直到8能整除8,所以从8开始,即8-15字节存储变量b
    因此HLStruct4的需要的内存大小为16字节,而HLStruct4最大变量的字节数为8HLStruct4实际的内存大小必须是8的整数倍16刚好为8的倍数,所以sizeof(HLStruct4)的结果是 16
    HLStruct5内存计算
  • 变量a:占1个字节,从0开始,即0字节存储变量a
  • 变量b:占2个字节,1不能整除2,故往后顺移,直到2能整除2,所以从2开始,即2-3字节存储变量b
  • 变量c:占16个字节,HLStruct4中最大的成员大小为8,所以c要从8的整数倍开始,当前是从4开始,所以不符合要求,需要往后移动到8,8是8的整数倍,符合内存对齐原则,即8-23存储变量c
    因此HLStruct5的需要的内存大小为24字节,而HLStruct4最大变量的字节数为8HLStruct5实际的内存大小必须是8的整数倍24刚好为8的倍数,所以sizeof(HLStruct5)的结果是 24
    下图为输出及打印
    image.png

内存优化(属性重排)

根据内存对齐原则,HLStruct1补齐了9个字节,而HLStruct2只补齐1个字节即可满足该规则,因此得出一个结论结构体内存大小与结构体成员内存大小的顺序有关

创建一个对象来探索

  1. 首先定义一个自定义类HLPerson类,并添加几个属性
@interface HLPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;

@property (nonatomic) char c1;
@property (nonatomic) char c2;

@end
  1. 在main中创建HLPerson的实例对象,并对其属性赋值
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        HLPerson *person = [HLPerson alloc];
        person.name      = @"HL";
        person.nickName  = @"Lay";
        person.age       = 17;
        person.height    = 180;
        person.c1        = 'a';
        person.c2        = 'b';

        NSLog(@"%@", person);
    }
    return 0;
}
  1. 断点调试person
  • x personxmemory read的简写,读取内存信息
    tips:iOS是小端模式,所以内存的读取要反着读,即cd 83 00 00 01 80 1d 00 应该读取为 0x001d8001000083cd
    image.png
  • x/8gx person:以16进制打印8行内存信息,并分别打印其指向,此方法读取地址更为便捷
    image.png

    这里虽然打印了8行内存信息,但实际上person对象变量并没有使用这么多内存,可以通过class_getInstanceSize方法获取实际上该对象只占用了40字节的内存,也就是上图中前五段内存,所以后三段全都为0x00000000,但是有几个属性值却并没有找到。
    分析
    没有找到agec1c2对应的值,是不是苹果做了什么处理避免内存过度消耗,我们用没有正常输出信息的内存尝试解析下
    image.png

    结论
    namenicknameheight都是各自占用8字节。可以直接打印出来;而ageInt占用4字节,c1c2char,各自占用1字节。我们推测系统可能进行属性重排,将他们存放在了一个块区。
    下图是HLPerson的内存分布情况
    image.png

    特殊的doublefloat
    height属性类型修改为double
//@property (nonatomic, assign) long height;
@property (nonatomic, assign) double height;

重新运行

image.png

直接po打印0x4066800000000000,并不能正确输出变量height的值,这是因为编译器po打印默认当做int类型处理

  • p/x (double)180:将180转成double类型然后以16进制进行打印,发现地址完全一样。
    image.png
    height改成float类型也可以用p/x (float)180验证
    封装2个验证函数
// float转换为16进制
void hl_float2HEX(float f) {
    union uuf { float f; char s[4]; } uf;
    uf.f = f;
    printf("0x");
    for (int i = 3; i >= 0; i--) {
        printf("%02x", 0xff & uf.s[i]);
    }
    printf("\n");
}

// double转换为16进制
void hl_double2HEX(double d) {
    union uud { double d; char s[8]; } ud;
    ud.d = d;
    printf("0x");
    for (int i = 7; i >= 0; i--) {
        printf("%02x", 0xff & ud.s[i]);
    }
    printf("\n");
}

打印验证


image.png

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

objc4源码中搜索class_getInstanceSize,可以在runtime.h找到:

/** 
 * Returns the size of instances of a class.
 * 
 * @param cls A class object.
 * 
 * @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
 */
OBJC_EXPORT size_t
class_getInstanceSize(Class _Nullable cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

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

// 其中 WORD_MASK 为
#   define WORD_MASK 7UL

通过源码可以看出,对于一个对象来说,其真正的对齐方式是8字节对齐,8字节对齐已经足够满足对象的需求了
总结
class_getInstanceSize:是采用8字节对齐,参照的对象的属性内存大小
malloc_size:采用16字节对齐,参照的整个对象的内存大小,对象实际分配的内存大小必须是16的整数倍

内存对齐算法

至此,我们已知的16字节对齐算法有两种

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

align16

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

segregated_size_to_fit

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 + (16 - 1) >> 4 << 4 ,其中右移4+左移4相当于将后4位抹零,跟k / 16 * 16一样 ,小于16就成0了

你可能感兴趣的:(OC底层原理04 - 内存对齐)