iOS底层原理02-怎么就内存对齐了呢

怎么查看内存
算法

  • 经过sizeof能够获取基本数据类型的内存占用,通常用于查看栈空间中基本数据类型内存状况:
    //  1. 基本数据:
    NSLog(@"BOOL:%lu",sizeof(BOOL));  // BOOL:1
    NSLog(@"short:%lu",sizeof(short));  // short:2
    NSLog(@"int:%lu",sizeof(int));  // int:4
    NSLog(@"long:%lu",sizeof(long));  // long:8
    NSLog(@"float:%lu",sizeof(float));  // float:4
    NSLog(@"double:%lu",sizeof(double));  // double:8

    // 2.结构体数据:
    struct TheStructOne{
        int one;
    }TheStructOne;
    struct TheStructTwo{
        int one;
        int two;
    }TheStructTwo;
    struct TheStructThree{
        int one;
        int two;
        int three;
    }TheStructThree;

    NSLog(@"TheStructOne:%lu",sizeof(TheStructOne));  // TheStructOne:4
    NSLog(@"TheStructTwo:%lu",sizeof(TheStructTwo));  // TheStructTwo:8
    NSLog(@"TheStructThree:%lu",sizeof(TheStructThree));  // TheStructThree:12

  • 经过class_getInstanceSize方法能够对象在堆空间中实际占用的内存状况,经过malloc_size能够获取实际为对象开辟的内存空间状况,这二者都只对对象有效,通常用于查看堆空间中对象内存状况:
    //  0. 定义一个函数打印内存占用状况
    void printMemory(NSObject *objc)
    {
        NSLog(@"类型:%@->类型占用,实际占用,实际分配:%lu %lu %lu",objc.class,sizeof(objc),class_getInstanceSize([objc class]),malloc_size((__bridge const void*)(objc)));
    }

    //  1.基类内存状况
    printMemory([NSObject alloc]);  // 类型:NSObject->类型占用,实际占用,实际分配:8 8 16

    //  2.自定义类内存状况
    @interface TheObjectOne : NSObject
    {
        int one;    // 4
    }
    @end

    @interface TheObjectTwo : NSObject
    {
        int one;    // 4
        int two;    // 4
    }
    @end

    @interface TheObjectThree : NSObject
    {
        int one;    // 4
        int two;    // 4
        int three;  // 4
    }
    @end

    printMemory([TheObjectOne alloc]); // 类型:TheObjectOne->类型占用,实际占用,实际分配:8 16 16
    printMemory([TheObjectTwo alloc]); // 类型:TheObjectTwo->类型占用,实际占用,实际分配:8 16 16

    printMemory([TheObjectThree alloc]); // 类型:TheObjectThree->类型占用,实际占用,实际分配:8 24 32
    printMemory([TheObjectFour alloc]); // 类型:TheObjectFour->类型占用,实际占用,实际分配:8 24 32

    printMemory([TheObjectFiv alloc]); // 类型:TheObjectFiv->类型占用,实际占用,实际分配:8 32 32
    printMemory([TheObjectSix alloc]); // 类型:TheObjectSix->类型占用,实际占用,实际分配:8 32 32

    printMemory([TheObjectSeven alloc]); // 类型:TheObjectSeven->类型占用,实际占用,实际分配:8 40 48
    printMemory([TheObjectEight alloc]); // 类型:TheObjectEight->类型占用,实际占用,实际分配:8 40 48

    printMemory([TheObjectNine alloc]); // 类型:TheObjectNine->类型占用,实际占用,实际分配:8 48 48
    printMemory([TheObjectTen alloc]); // 类型:TheObjectTen->类型占用,实际占用,实际分配:8 48 48

内存怎么是这样的

好像有的结论

  1. 结构体实际占用空间大小是各基础数据类型的总大小之和。
  2. 对象类型恒定占用为8,实际占用空间最低8,实际分配最低16。而实际占用空间以8为跨度增加,实际分配空间最低以16为跨度增加。

那么是否是呢?

结构体占用大小

咱们看看结构体,将结构体数据类型换成其它基础类型:xcode

    struct TheStructCharOne{
        char one;
    }TheStructCharOne;

    struct TheStructCharTwo{
        char one;
        int two;
    }TheStructCharTwo;

    struct TheStructCharThree{
        char one;
        int two;
        int three;
    };

    NSLog(@"%lu",sizeof(TheStructCharOne));     // 1
    NSLog(@"%lu",sizeof(TheStructCharTwo));     // 8
    NSLog(@"%lu",sizeof(TheStructCharThree));   // 12

和咱们想的不同,大小从1忽然就到8了,有点像类实例分配时候增加的跨度。架构

类占用大小

首先经过命令 clang -rewrite-objc main.m -o main.cpp 将main.m编译为C++,为了方便查看 咱们将编译后的main.cpp拖入到xcode工程中,而后为了解除编译报错,从编译源码中移除: 编译源码中移除main.cppide

咱们能够看到编译后的TheObjectThree变成了:函数

    struct NSObject_IMPL {
        Class isa;  // 8
    };
    struct TheObjectThree_IMPL {
        struct NSObject_IMPL NSObject_IVARS;
        int one;    // 4
        int two;    // 4
        int three;  // 4
    };

结构体包含结构体等价于把子结构体的全部成员所有放入父结构体中,因此至关于:

    struct TheObjectThree_IMPL {
        struct NSObject_IMPL {
            Class isa;  // 8
        };
        int one;    // 4
        int two;    // 4
        int three;  // 4
    };

能够看出来类的本质是结构体,只不过自定义类相比普通结构体都会多出一个8字节的isa成员。因此搞清楚告终构体为何实际分配内存空间比成员结构实际占用空间计算出来的大,就能搞清楚类的相同状况。优化

内存对齐是什么

咱们先想想计算机是怎么读取内存的。不一样的处理器根据处理能力不一样会一次性读取固定位数的内存块,这个咱们称之为内存存取粒度。ui

咱们知道了内存是按块来读取的,如今假设一个结构的大小恰好在一个内存块的大小范围内,理想状况下只须要一次就能读取成功,但若是它的起始位置在上一个内存块,结束在另外一个块,那么这个CPU只能读取两次,带来了存取效率上的损失,并且中间还会作剔除和合并。因此为了提升效率及方便读取,咱们须要将这个内存进行对齐,使其能在最小读取次数内进行访问。code

对于某些架构的CPU甚至会发生变量不对齐就报错,这种状况下只能保证能存对齐。

对齐规则

内存对齐原则对象

  • 数据成员对齐原则: 结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,之后每一个数据成员存储的起始位置要从该成员大小或者成员的子成员大小.
  • 结构体做为成员:若是一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.
  • 结构体的总大小,也就是sizeof的结果,必须是其内部最大 成员的整数倍,不足的要补⻬.

总之,内存对齐原则就是:min(m,n) //m为开始的位置,n为所占位数。当m是n的整数倍时,条件知足;不然m位空余,m+1,继续min算法:blog

结构体内存存储状况

如MyStruct1实际占用计算过程:

    a:从0开始,此时min(0,1),即[0]存储a
    b:从1开始,此时min(1,8),1%8!=0,继续日后移动,直到min(8,8),即[8-15]存储b
    c:从16开始,此时min(16,4),16%4=0,即[16-19]存储c
    d:从20开始,此时min(20, 2),20%2=0,即[20-21]存储d

经过这一步骤,咱们能够作一些优化重排的工做:

    struct Optimize1 {
        double a;   // 8
        int b;      // 4
        bool c;     // 1
    }Optimize1;
    /*
        min(0,8)=a=>[0-7],min(8,4)=b=>[8-11],min(12, 1)=c=>[12];
        实际大小为13bytes,最大变量的a字节数为8,最小的知足8的整数倍的是16,Optimize1分配内存为16bytes.
    */

    struct Optimize2 {
        int d;      // 4
        double e;   // 8
        bool f;     // 1
    }Optimize2;
    /*
        min(0,4)=d=>[0-3],min(4,8)=e=>[8-15],min(16, 1)=f=>[16];
        实际大小为17bytes,最大变量的a字节数为8,最小的知足8的整数倍的是24,Optimize2分配内存为24bytes.
    */

这里咱们想到了类的结构体嵌套,试着根据规则计算结构体嵌套:

    struct Optimize3 {
        double a;   // 8
        int b;      // 4
        bool c;     // 1
        struct Optimize2 opt2;  // 24
    }Optimize3;     // 结构体大小:40,结构体成员大小opt2:24
    /*
        min(0,8)=a=>[0-7],min(8,4)=b=>[8-11],min(12, 1)=c=>[12];
        min(16,24)=opt2=>[16,40]    
    */

    struct Optimize4 {
        struct Optimize2 opt2;  // 24
        double a;   // 8
        int b;      // 4
        bool c;     // 1
    }Optimize4; // 结构体大小:40,结构体成员大小opt2:24
    /*
        min(0,24)=opt2=>[0,23];
        min(24,8)=a=>[24-31],min(32,4)=b=>[32-35],min(36, 1)=c=>[36];   // 40
    */

    struct Optimize5 {
        double a;   // 8
        int b;      // 4
        bool c;     // 1
        int d;      // 4
        double e;   // 8
        bool f;     // 1
    }Optimize5;     // 结构体大小:40
    /*
        min(0,8)=a=>[0-7],min(8,4)=b=>[8-11],min(12, 1)=c=>[12];
        min(13,4)=d=>[16-19],min(20,8)=e=>[24-31],min(32, 1)=f=>[32];   // 40
    */

能够看出来结构体的嵌套等同于将从子结构体中将成员变量直接放到父成员变量中,因此TheObjectOne_IMPL结构体等价与:

    struct TheObjectOne_IMPL {
        Class isa;  // 8
        int one;    // 4
    };  // 16

根据以上规则,NSObject类对应的NSObject_IMPL结构体对应的类型占用/实际占用/实际分配应该为8/8/8,为什么NSObject最后打印出来的结果是8/16/16呢?经过OC源码,咱们窥探一下:

    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());
    }

    // May be unaligned depending on class's ivars.
    uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }

    #   define WORD_MASK 7UL
    static inline uint32_t word_align(uint32_t x) {
        return (x + WORD_MASK) & ~WORD_MASK;
    }

能够看到,class_getInstanceSize就是获取实例对象中成员变量的内存大小。 而(x + WORD_MASK) & ~WORD_MASK 至关于(x+7) & ~7:

8的二进制  0000 1000 后三位都是0
7的二进制  0000 0111 后三位都是1
~7的后三位都是000,通过&运算后三位必定是000,最后的结果一定是8的倍数.
(x+7)的含义是任意一个数给你最大的可能性升阶(8的n阶乘).
如1+7=8,8的一阶.11+7=18.就是8的2阶2*8=16.相比16相差2,因此后三位的就无论了直接&运算就抹零了.

因此class_getInstanceSize最小返回为16.根据16字节对齐规则咱们能够推断出类的起始地址以0开始。

我想影响内存对齐

这里有两个编译指令:

#pragma pack(n) 
    n就是你要指定的“对齐系数”,一次性能够从内存中读/写n个字节.n=1,2,4,8,16.
#pragma pack() 
    取消自定义字节对齐.

__attribute__((aligned (n)))
    让所做用的结构成员对齐在n字节天然边界上.
__attribute__((packed))
    取消优化对齐,按照实际占用字节数对齐.

内存对齐的"对齐数"取决于"对齐系数"和"成员的字节数"二者之中的较小值。

相关文章
相关标签/搜索