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))
    取消優化對齊,按照實際佔用字節數對齊.

內存對齊的"對齊數"取決於"對齊係數"和"成員的字節數"二者之中的較小值。

相關文章
相關標籤/搜索