OC底层原理六: 内存对齐

作者: markhetao | 来源:发表于2020-09-13 00:13 被阅读0次

OC底层原理 学习大纲

前期准备

1.lldb打印规则

po: 对象信息

(lldb) po person
<HTPerson: 0x101875c70>

p: 对象信息

(lldb) p person
(HTPerson *) $1 = 0x0000000101875c70

xmemory read的简写,读取内存信息 (iOS是小端模式,内存读取要反着读)
例如: e5 22 00 00 01 80 1d 00 应读取为0x001d8001000022e5

(lldb) memory read person
0x1024aef20: c9 21 00 00 01 80 1d 00 00 00 00 00 00 00 00 00  .!..............
0x1024aef30: 2d 5b 4e 53 56 69 73 75 61 6c 54 61 62 50 69 63  -[NSVisualTabPic
(lldb) x person
0x1024aef20: c9 21 00 00 01 80 1d 00 00 00 00 00 00 00 00 00  .!..............
0x1024aef30: 2d 5b 4e 53 56 69 73 75 61 6c 54 61 62 50 69 63  -[NSVisualTabPic

x/4gx: 打印4条16进制的16字符长度的内存信息

(lldb) x/4gx person
0x101875c70: 0x001d8001000022e5 0x0000000000000012
0x101875c80: 0x0000000100001010 0x0000000100001030

x/4gw: 打印4条16进制的8字符长度的内存信息

(lldb) x/4gw person
0x1024aef20: 0x000021c9 0x001d8001 0x00000000 0x00000000

p/t: 二进制打印

(lldb) p/t person
(HTPerson *) $2 = 0b0000000000000000000000000000000100000010010010101110111100100000
2.获取内存大小
  • sizeof:

    操作符。传入数据类型,输出内存大小。编译时固定
    只与类型相关,与具体数值无关。(如:bool 2字节,int 4字节,对象(指针)8字节)

  • class_getInstanceSize:

    runtime的api,传入对象,输出对象所占的内存大小,本质是对象中成员变量的大小

  • malloc_size:

    获取系统实际分配的内存大小,符合前面章节align16对齐标准

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // insert code here...
        NSObject * objc = [[NSObject alloc] init];
        NSLog(@"[sizeof]                 内存大小: %lu字节", sizeof(objc));
        NSLog(@"[class_getInstanceSize]  内存大小: %lu字节", class_getInstanceSize([objc class]));
        NSLog(@"[malloc_size]            内存大小: %lu字节", malloc_size((__bridge const void *)(objc)));
    }
    return 0;
}
image.png
  • 今天我们就来了解,对象内部内存对齐

内存对齐

我们知道对象对外,苹果系统会采用align16字节对齐开辟内存大小,提高系统存取性能。

对象内部呢?

  • 对象的本质是结构体,这个在后续篇章中我们会详细了解。所以研究对象内部的内存,就是研究结构体内存布局

  • 内存对齐目的:最大程度提高资源利用率

我们从一个小案例开始入手

struct MyStruct1 {
    char a;       // 1字节
    double b;     // 8字节
    int c;        // 4字节
    short d;      // 2字节
    NSString * e; // 8字节(指针)
} MyStruct1;

struct MyStruct2 {
    NSString * a; // 8字节(指针)
    double b;     // 8字节
    int c;        // 4字节
    short d;      // 2字节
    char e;       // 1字节
} MyStruct2;

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        NSLog(@"%lu - %lu", sizeof(MyStruct1), sizeof(MyStruct2));
    }
    return 0;
}

打印结果:

image.png
MyStruct1 和 MyStruct2 的构成元素都一样,为何打印出的内存大小不一致?
  • 结构体内部的元素排序影响内存大小。这就是内存字节对齐的作用。
结构体内存对齐规则

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

注意: 这里的8字节对齐是结构体内部对齐规则,对象在系统中对外实际分配的空间是遵循16字节对齐原则。

【三条结构体对齐规则】:
(先把规则写出来,我们下面用实例来理解)

  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. 最后结构体的内存大小必须是结构体中最大成员内存大小的整数倍,不足的需要补齐。

iOS 基础数据类型 字节数表

基础数据类型字节数 MyStruct1 内存计算 MyStruct2 内存计算

结构体中的结构体

struct MyStruct3 {
    NSString * a; // 8字节(指针)
    double b;     // 8字节
    int c;        // 4字节
    short d;      // 2字节
    char e;       // 1字节
    struct MyStruct2 str;
} MyStruct3;

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        NSLog(@"MyStruct3内存大小: %lu", sizeof(MyStruct3));
        NSLog(@"MyStruct3中的结构体(MyStruct2)内存大小 %lu", sizeof(MyStruct2));
    }
    return 0;
}
image.png MyStruct3 内存计算

内存优化(属性重排)

  • 我们观察到MyStruct1MyStruct2的成员属性一样,但是在内存管理上,MyStruct2MyStruct1利用率更高(白色空白区域更少)。

  • MyStruct2intshortchar 4 + 2 + 1组合,空间利用得更合理。

  • 苹果会进行属性重排,对属性进行合理排序,尽可能保持保持属性之间的内存连续,减少padding(白色部分,属性之间置空的内存)。

如果你还记得align16对齐方式,你应该能理解属性重排的好处了

  • align16, 是空间换取时间,保障系统在处理对象时能快速存取
  • 属性重排,保障一个对象尽可能少的占用内存资源。

属性重排案例

  • 创建HTPerson
@interface HTPerson : NSObject

@property(nonatomic, copy)   NSString * name;
@property(nonatomic, copy)   NSString * nickname;
@property(nonatomic, assign) int        age;
@property(nonatomic, assign) long       height;
@property(nonatomic, assign) char       c1;
@property(nonatomic, assign) char       c2;

@end
  • main.m 加入测试代码
#import "HTPerson.h"

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        HTPerson * person = [[HTPerson alloc]init];
        person.age      = 18;
        person.height   = 190;
        person.name     = @"mark";
        person.nickname = @"哈哈";
        person.c1       = 'A';
        person.c2       = 'B';
        
        NSLog(@"%@", person);
    }
    return 0;
}
  • x/8gx person: 16进制打印8行内存信息

    image.png
  • 我们分析属性,namenicknameheight都是各自占用8字节。可以直接打印出来。

  • age是Int占用4字节,c1c2是char,各自占用1字节。我们推测系统可能属性重排,将他们存放在了一个块区。

image.png

特殊的doublefloat

我们尝试把height属性类型修改为double

@property(nonatomic, assign) double     height;
image.png
我们发现直接po打印0x4067c00000000000,打印不出来height的数值190。 这是因为编译器po打印默认当做int类型处理。
  • p/x (double)190:我们以16进制打印double类型值打印,发现完全相同。

如果height熟悉换成float,也是一样的使用p/x (float)190验证。

我们可以封装2个验证函数:

// float转换为16进制
void ht_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");
}

// float转换为16进制
void ht_double2HEX(float f){
    union uuf { float f; char s[8];} uf;
    uf.f = f;
    printf("0x");
    for (int i = 7; i>=0; i--) {
        printf("%02x", 0xff & uf.s[i]);
    }
    printf("\n");
}
image.png

为什么对象内部字节对齐是8字节

我们在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);

objc-class.mm可以找到:

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

进入alignedInstanceSize:

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

进入word_align

#ifdef __LP64__ // 64位操作系统
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL  // 7字节遮罩
#   define WORD_BITS 64 
#else 
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

static inline uint32_t word_align(uint32_t x) {
    // (x + 7) & (~7)  --> 8字节对齐
    return (x + WORD_MASK) & ~WORD_MASK;
}

可以看到,系统内部设定64位操作系统,统一使用8字节对齐

总结

  • 外部处理,系统面对的对象太多,我们统一按照align16内存块来存取,效率很快。(所以malloc_size读取的都是16的倍数)

  • 但为了避免浪费太多内存空间。系统会在每个对象内部进行属性重排,并使用8字节对齐,使单个对象占用的资源尽可能小。(所以class_getInstanceSize读取的都是8的倍数)

  • 外部使用16字节对齐,给类留足够间距,避免越界访问,对象内部使用8字节对齐完全足够。

至此, OC底层原理三:探索alloc (你好,alloc大佬 )中提到的三大核心方法,我们已掌握了initstanceSize计算内存大小。

_class_createInstanceFromZone核心方法.png

下一节: ` OC底层原理七: malloc源码分析

相关文章

  • iOS--OC底层原理文章汇总

    OC底层原理01—alloc + init + new原理OC底层原理02—内存对齐OC底层原理03— isa探究...

  • OC底层原理汇总

    OC底层原理(一).alloc实际调用流程分析OC底层原理(二).内存分配与内存对齐OC底层原理(三)、isa、对...

  • OC底层原理六: 内存对齐

    OC底层原理 学习大纲 前期准备 1.lldb打印规则 po: 对象信息 p: 对象信息 x: memory r...

  • OC底层原理--内存对齐

    既然是底层原理系列,内存肯定是我们绕不过的一个知识点,今天这篇文章主要是通过源码来探索下OC底层是怎么进行内存对齐...

  • OC底层原理-内存对齐

    在探讨内存对齐原理之前,首先介绍下iOS中获取内存大小的三种方式 获取内存大小的三种方式 获取内存大小的三种方式分...

  • OC底层原理 05: 内存对齐原理

    主动已经是我对热爱东西表达的极限了 通过对结构体内存是如何对齐的?打开内存对齐原理的大门。 在探究内存对齐之前,先...

  • OC底层原理 04:内存对齐原理

    在探讨内存对齐原理之前,首先介绍下iOS中获取内存大小的三种方式 获取内存大小的三种方式 获取内存大小的三种方式分...

  • OC底层原理02—内存对齐

    为什么要内存对齐? 1.性能方面:提升CPU读取速度。CPU是按照块来读取的,块的大小可以为2,4,8,16。块的...

  • OC底层原理04 - 内存对齐

    获取内存大小的三种方式 sizeof class_getInstanceSize malloc_size size...

  • OC对象内存占用及优化

    结构体内存对齐原理 前言:我们都知道,在iOS开发中,我们写的oc代码,底层都是用c++来实现的,而oc对象本质就...

网友评论

    本文标题:OC底层原理六: 内存对齐

    本文链接:https://www.haomeiwen.com/subject/qemiektx.html