美文网首页iOS
iOS-底层原理5:内存对齐

iOS-底层原理5:内存对齐

作者: AcmenL | 来源:发表于2020-09-23 18:16 被阅读0次

    问题

    一个NSObject对象占用了多少内存?

    分析

    先了解下获取内存的方式
    • sizeof
    • class_getInstanceSize
    • malloc_size
    #import <Foundation/Foundation.h>
    #import "LBHPerson.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(@"内存大小:%lu",sizeof(objc));
            NSLog(@"内存大小:%lu",class_getInstanceSize([objc class]));
            NSLog(@"内存大小:%lu",malloc_size((__bridge const void*)(objc)));
            
        }
        return 0;
    }
    
    运行结果

    三种方式获取的内存大小不一样,

    三种获取内存方式的差别
    方式 说明
    sizeof 只与类型相关,与具体数值无关。(如:bool 2字节,int 4字节,对象(指针)8字节)
    class_getInstanceSize runtime的api,用于获取类的实例对象所占用的内存大小,并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小 8字节对齐
    malloc_size 获取系统实际分配的内存大小 16字节对齐
    源码分析

    command+鼠标单击跳转到NSObject的定义

    @interface NSObject <NSObject> {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
        Class isa  OBJC_ISA_AVAILABILITY;
    #pragma clang diagnostic pop
    }
    

    简化一下

    @interface NSObject  {
        Class isa ;
    }
    

    转成C++代码

    Clang -rewrite-objc main.m -o main.cpp
    

    或转成arm64架构的手机上运行的C++代码

    //xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc  OC源文件  -o  输出的CPP文件
    xcrun  -sdk  iphoneos clang  -arch arm64  -rewrite-objc main.m -o main-arm64.cpp
    

    从转换的C++文件中找到对应的NSObject结构

    struct NSObject_IMPL {
            //
        Class isa;
    };
    
    //class 是一个结构体指针
    typedef struct objc_class *Class;
    
    
    • obj是一个指针,指针占8个字节,所以sizeof为8字节
    • NSObject是一个结构体,里面只包含一个isa指针,一个指针8个字节,所以class_getInstanceSize为8字节
    • class_getInstanceSize占8个字节,但malloc_size16字节对齐,大小应为16字节的倍数,所以为16字节;

    但如果情况更复杂呢?例如结构体中包含多个变量,它们的内存又是大多?这就是本文要讲的重点内存对齐。

    内存对齐

    iOS-底层原理2:alloc、init、new探析中介绍过内存对齐,这里进行更深入的探索。

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

    //1、定义两个结构体
    struct Mystruct1{
        char a;     //1字节
        double b;   //8字节
        int c;      //4字节
        short d;    //2字节
    }Mystruct1;
    
    struct Mystruct2{
        double b;   //8字节
        int c;      //4字节
        short d;    //2字节
        char a;     //1字节
    }Mystruct2;
    
    //计算 结构体占用的内存大小
    NSLog(@"%lu-%lu",sizeof(Mystruct1),sizeof(Mystruct2));
    

    运行结果


    运行结果

    连个结构体只是其中变量顺序变化了一下,内存大小就发生了变化,这就是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的整数倍地址开始存储)

    可以理解为min(m, n) 的公式, 其中 m表示当前成员的开始位置, n表示当前成员所需要的位数。如果满足条件 m 整除 n (即 m % n == 0), n 从 m 位置开始存储, 反之继续检查 m+1 能否整除 n, 直到可以整除, 从而就确定了当前成员的开始位置。

    • 数据成员为结构体:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储)
    • 结构体的整体对齐规则:结构体的总大小,即sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐
    分析

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

    根据内存对齐原则,用如图所示表示两个结构体的内存存储

    结构体内存存储情况
    结构体嵌套结构体

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

    定义一个结构体MyStruct3,在MyStruct3中嵌套MyStruct2,如下所示

    //结构体嵌套结构体
    struct Mystruct3{
        double b;   //8字节
        int c;      //4字节
        short d;    //2字节
        char a;     //1字节
        struct Mystruct2 str; 
    }Mystruct3;
    
    • 分析Mystruct3的内存

    变量b:占8个字节,从0开始,即0-7字节存储变量b
    变量c:占4个字节,从8开始, 8%4 == 0,即8-11字节存储变量c;
    变量d:占2个字节,从12开始,12%2==0,即12-13 存储变量d
    变量a:占1个字节,从14开始,14%1==0,即 14 存储变量a;
    变量str:str是一个结构体,根据内存对齐原则二,结构体成员要从其内部最大成员大小的整数倍开始存储,而MyStruct2中最大的成员大小为8,所以str要从8的整数倍开始,当前是从15开始,所以不符合要求,需要往后移动到16,16是8的整数倍,符合内存对齐原则,所以 16-31字节存储 str

    因此MyStruct3的需要的内存大小为 32字节,根据内存对其原则三
    Mystruct3实际的内存大小必须是最大成员b的整数倍,即必须是8的整数倍,所以sizeof(Mystruct3)的结果是 32;

    根据分析MyStruct3结构体变量内存存储如图所示:

    MyStruct3内存存储情况

    输出

    NSLog(@"%lu-%lu",sizeof(Mystruct3),sizeof(Mystruct3.str));
    

    输出结果

    输出结果

    输出结果与分析的一致

    二次验证

    为了保险起见,我们再定义一个结构体,来验证我们结构体嵌套的内存大小

    struct Mystruct4{
        double a;
        short b;
        char c;
        struct Mystruct5{   
            short e;
            char f;  
        }Mystruct5;
    }Mystruct4;
    
    • 分析
      变量a:占8字节,从0开始,即0-7字节存储变量a
      变量b:占2字节,从8开始,min(8,2),可以整除,即 8-9存储b;
      变量c:占1字节,从10开始,min(10,1),可以整除,即10存储c;
      结构体Mystruct5:所以从11开始,根据内存对齐原则二,即存储开始位置必须是最大的整数倍(最大成员为2),min(11,2)不能整除,继续往后移动,到12, min(12,2)满足,所以Mystruct5开始位置为12
      • 变量e:占2字节,从12开始min(12,2),可以整除,即12-13存储e;
      • 变量f:占1字节,从14开始min(14,1),可以整除,即14存储f;

    因此Mystruct4中需要的内存大小是15字节,根据内存对其原则三Mystruct4实际的内存大小必须是最大成员a的整数倍,即必须是8的整数倍,所以sizeof(Mystruct4)的结果是 16
    Mystruct5内存大小是3字节,根据内存对其原则三,Mystruct5实际的内存大小必须是最大成员e的整数倍,即必须是2的整数倍,所以sizeof(Mystruct5)的结果是 4

    输出

    NSLog(@"%lu--%lu",sizeof(Mystruct4),sizeof(Mystruct4.Mystruct5));
    

    输出结果

    内存优化

    MyStruct1补齐了9个字节,而MyStruct2只补齐一个字节即可满足字节对齐规则,这里得出一个结论结构体内存大小与结构体成员内存大小的顺序有关

    如果结构体中数据成员是根据内存从大到小的顺序定义的,根据内存对齐规则来计算结构体内存大小,我们只需要补齐少量内存padding即可满足堆存对齐规则,这种方式就是苹果中采用的,利用空间换时间,将类中的属性进行重排,来达到优化内存的目的

    分析

    先介绍下LLDB常见的指令,方便接下来的断点调试

    LLDB常见命令

    step1:定义一个自定义LBHPerson类,并定义几个属性

    @interface LBHPerson : NSObject
    
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, copy) NSString *nickName;
    // @property (nonatomic, copy) NSString *hobby;
    @property (nonatomic, assign) int age;
    @property (nonatomic, assign) long height;
    
    @property (nonatomic) char c1;
    @property (nonatomic) char c2;
    @end
    
    @implementation LBHPerson
    
    @end
    

    step2:在main函数中创建类的实例并给属性赋值

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            LBHPerson *person = [LBHPerson alloc];
            person.name      = @"liu";
            person.nickName  = @"666";
            person.age       = 18;
            person.height    = 190;
            person.c1        = 'a';
            person.c2        = 'b';
    
            NSLog(@"%@",person);
        }
        return 0;
    }
    

    step3:断点调试person

    • x personxmemory read的简写,读取内存信息 (iOS是小端模式,内存读取要反着读)
      95 47 00 00 01 80 1d 00应读取为0x001d800100004795

    • x/8gx person:16进制打印8行内存信息

    这里打印了8行内存信息,但实际上person对象变量并没有使用这么多内存,可以通过class_getInstanceSize方法获取实际上对象变量只是用了40字节的内存,就是上图中前五段内存,但是有几个属性值并没有找到。

    分析:没有找到age、c1及c2对应的值,是不是苹果做了什么处理避免内存过度消耗,我们用没有正常输出信息的内存尝试解析下

    结论:name、nickname、height都是各自占用8字节。可以直接打印出来;而age是Int占用4字节,c1和c2是char,各自占用1字节。我们推测系统可能属性重排,将他们存放在了一个块区

    特殊的doublefloat

    height属性类型修改为double

    //@property (nonatomic, assign) long height; 
    @property (nonatomic, assign) double height; 
    

    重新运行

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

    • p/x (double)190:190转成double类型然后以16进行打印,发现地址完全一样。
      height改成float类型也可以用p/x (float)190验证

    封装2个验证函数:

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

    结果

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

    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) {
        //x+7 & (~7) --> 8字节对齐
        return (x + WORD_MASK) & ~WORD_MASK;
    }
    
    
    //其中 WORD_MASK 为
    #   define WORD_MASK 7UL
    

    通过源码可知:

    对于一个对象来说,其真正的对齐方式 是 8字节对齐,8字节对齐已经足够满足对象的需求了

    总结
    • class_getInstanceSize:是采用8字节对齐,参照的对象的属性内存大小
    • malloc_size:采用16字节对齐,参照的整个对象的内存大小,对象实际分配的内存大小必须是16的整数倍,采用8字节对齐,两个对象的内存会紧挨着,显得比较紧凑,而16字节比较宽松,利于苹果以后的扩展。

    16字节内存对齐算法

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

    • alloc源码分析中的align16
    • malloc源码分析中的segregated_size_to_fit
    align16: 16字节对齐算法

    iOS-底层原理2:alloc、init、new探析
    一文中已经讲解过

    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
        }
    //  (size + 15) >> 4
        k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    //  << 4
        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了

    相关文章

      网友评论

        本文标题:iOS-底层原理5:内存对齐

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