OC底层原理04 - 内存对齐

2020-11-25  本文已影响0人  H雷610

获取内存大小的三种方式

sizeof

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

class_getInstanceSize

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

malloc_size

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

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

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

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

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:

结构体嵌套结构体

上面的两个结构体只是简单的定义数据成员,下面来一个比较复杂的,结构体中嵌套结构体的内存大小计算情况
定义一个结构体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内存计算

二次验证

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

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

HLStruct4内存计算

内存优化(属性重排)

根据内存对齐原则,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
//@property (nonatomic, assign) long height;
@property (nonatomic, assign) double height;

重新运行

image.png
直接po打印0x4066800000000000,并不能正确输出变量height的值,这是因为编译器po打印默认当做int类型处理
// 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字节对齐算法有两种

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了

上一篇 下一篇

猜你喜欢

热点阅读