iOS-底層原理:內(nèi)存對齊

上篇文章中iOS-底層原理:alloc & init & new 源碼分析通過對alloc源碼的分析,可以得知alloc的主要目的就是開辟內(nèi)存,并且會通過size = cls->instanceSize(extraBytes)這一步來計(jì)算需要開辟的內(nèi)存大小。那么它在計(jì)算的過程中是怎么處理的呢,或者說要遵循什么樣的原則?下面我們來接著探索。

一切的一切我們還是從最初的代碼開始

alloc一個無屬性Person

此時我們只是創(chuàng)建了Person 類,其中什么信息都沒有。當(dāng)我們讀取內(nèi)存段,打印出的前8位是 Person,此時是我們的isa指針。

Person 增加 NSString 屬性

// Person 類中的屬性
@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;

@end
  • 首先是無賦值情況
    無賦值
  • 賦值一個屬性情況
    賦值一個屬性
  • 賦值兩個屬性情況
    賦值兩個屬性

通過打印我們得知一個NSString類型的屬性是占8位字節(jié)大小,
光是這點(diǎn)我們看不出什么,接著繼續(xù)。。

Person 增加 int 屬性

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;

@end
  • 先看下 無賦值情況
    int不賦值

這時候我們發(fā)現(xiàn)和之前只有NSString的時候不一樣了,第二個段內(nèi)存段會打印出 0x0000000000000000 ,接著繼續(xù)。。

  • age賦值
    age賦值

這時候我們發(fā)現(xiàn)本來第二塊的位置應(yīng)該是name,現(xiàn)在被age給占了?怎么不知道先來后到?為了想知道為什么接著往下走。。

再次在Preson屬性中增加個int類型的屬性,并賦值。

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;

@end
兩個int賦值

從打印上看此時兩個int類型ageheight共用一個8字節(jié)的內(nèi)存。

接著往下看。。

Person 增加 char 屬性

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@property (nonatomic) char s;
@property (nonatomic) char b;

@end

看下打印結(jié)果

char屬性賦值

這時候我們發(fā)現(xiàn)一個 int 類型的和兩個 char 類型的屬性共用了 8 字節(jié)的內(nèi)存空間。

看到這里我們腦子里有個大大的?,為什么會是這種情況,那么今天的重點(diǎn)來了。。

內(nèi)存對齊

計(jì)算機(jī)內(nèi)存都是以字節(jié)為單位劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但是實(shí)際的計(jì)算機(jī)系統(tǒng)對基本類型數(shù)據(jù)在內(nèi)存中存放的位置有限制,它們會要求這些數(shù)據(jù)的首地址的值是某個數(shù)k(通常它為48的倍數(shù),這就是所謂的內(nèi)存對齊

內(nèi)存對齊的原因

我們都知道內(nèi)存是以字節(jié)為單位,但是大部分處理器并不是按字節(jié)塊來存取內(nèi)存的.它一般會以2字節(jié),4字節(jié),8字節(jié),16字節(jié)甚至32字節(jié)為單位來存取內(nèi)存,我們將上述這些存取單位稱為內(nèi)存存取粒度。

  • CPU的數(shù)據(jù)總線寬度決定了CPU對數(shù)據(jù)的吞吐量
  • 64CPU一次處理64 bit也就是8個字節(jié)的數(shù)據(jù),32同樣,每次處理4個字節(jié)的數(shù)據(jù)

內(nèi)存對齊規(guī)則

每個特定平臺上的編譯器都有自己的默認(rèn)“對齊系數(shù)”(也叫對齊模數(shù))。程序員可以通過預(yù)編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數(shù),其中的n就是你要指定的“對齊系數(shù)”。在ios中,Xcode默認(rèn)為#pragma pack(8),即8字節(jié)對齊。

內(nèi)存對齊規(guī)則可理解為以下三點(diǎn):

  • 數(shù)據(jù)成員對?規(guī)則:數(shù)據(jù)成員的對齊規(guī)則可以理解為min(m, n) 的公式, 其中 m表示當(dāng)前成員的開始位置, n表示當(dāng)前成員所需要的位數(shù)。如果滿足條件 m 整除n (即m % n == 0), nm 位置開始存儲, 反之繼續(xù)檢查m+1能否整除n, 直到可以整除, 從而就確定了當(dāng)前成員的開始位置。
  • 結(jié)構(gòu)體作為成員:如果一個結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從
    其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲.(struct a里存有struct b,b
    里有char,int ,double等元素,那b應(yīng)該從8的整數(shù)倍開始存儲.)
  • 收尾工作:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,.必須是其內(nèi)部最大成員變量的整數(shù)倍.不足的要補(bǔ)齊.

下表是各種數(shù)據(jù)類型在ios中的占用內(nèi)存大小,根據(jù)對應(yīng)類型來計(jì)算結(jié)構(gòu)體中內(nèi)存大小,


數(shù)據(jù)類型對應(yīng)的字節(jié)數(shù)表格

上面一大堆都是理論,理解著比較困難,難么下面我們就通過實(shí)踐來分析。

結(jié)構(gòu)體內(nèi)存對齊

定義兩個結(jié)構(gòu)體

//1、定義兩個結(jié)構(gòu)體
struct Struct1{
    char a;     //1字節(jié)
    double b;   //8字節(jié)
    int c;      //4字節(jié)
    short d;    //2字節(jié)
}Struct1;

struct Struct2{
    double b;   //8字節(jié)
    int c;      //4字節(jié)
    short d;    //2字節(jié)
    char a;     //1字節(jié)
}Struct2;

//計(jì)算 結(jié)構(gòu)體占用的內(nèi)存大小
NSLog(@"%lu-%lu",sizeof(Struct1),sizeof(Struct2));

以下是輸出結(jié)果:

24 - 16

兩個結(jié)構(gòu)體乍一看,沒什么區(qū)別,其中定義的變量 和 變量類型都是一致的,唯一的區(qū)別只是在于定義變量的順序不一致,那為什么他們做占用的內(nèi)存大小不相等呢?其實(shí)這就是iOS中的內(nèi)存字節(jié)對齊現(xiàn)象

結(jié)構(gòu)體對應(yīng)的存儲情況

結(jié)構(gòu)體Struct1內(nèi)存大小計(jì)算

  • 根據(jù)內(nèi)存對齊規(guī)則計(jì)算Struct1的內(nèi)存大小,詳解過程如下:

    • 變量a:占1個字節(jié),從0開始,此時min(0,1),即 0存儲a
    • 變量b:占8個字節(jié),從1開始,此時min(1,8),1不能整除8,繼續(xù)往后移動,知道min(8,8),從8開始,即 8-15 存儲 b
    • 變量c:占4個字節(jié),從16開始,此時min(16,4)16可以整除4,即 16-19存儲 c
    • 變量d:占2個字節(jié),從20開始,此時min(20, 2),20可以整除2,即20-21 存儲d

因此Struct1的需要的內(nèi)存大小為 18字節(jié),而Struct1中最大變量的字節(jié)數(shù)為8,所以 Struct1實(shí)際的內(nèi)存大小必須是8的整數(shù)倍,18向上取整到24,主要是因?yàn)?code>24是8的整數(shù)倍,所以 sizeof(Struct1)的結(jié)果是 24

結(jié)構(gòu)體Struct2內(nèi)存大小計(jì)算

  • 根據(jù)內(nèi)存對齊規(guī)則計(jì)算Struct2的內(nèi)存大小,詳解過程如下:

    • 變量b:占8個字節(jié),從0開始,此時min(0,8),即 0-7 存儲b
    • 變量c:占4個字節(jié),從8開始,此時min(8,4)8可以整除4,即 8-11 存儲 c
    • 變量d:占2個字節(jié),從12開始,此時min(12, 2),20可以整除2,即12-13 存 儲d
    • 變量a:占1個字節(jié),從14開始,此時min(14,1),即 14存儲 a

因此Struct2的需要的內(nèi)存大小為15字節(jié),而Struct1中最大變量的字節(jié)數(shù)為8,所以 Struct2實(shí)際的內(nèi)存大小必須是 8的整數(shù)倍,15向上取整到16,主要是因?yàn)?6是8的整數(shù)倍,所以 sizeof(Struct2) 的結(jié)果是 16

結(jié)構(gòu)體嵌套結(jié)構(gòu)體

上面的兩個結(jié)構(gòu)體只是簡單的定義數(shù)據(jù)成員,下面來一個比較復(fù)雜的,結(jié)構(gòu)體中嵌套結(jié)構(gòu)體的內(nèi)存大小計(jì)算情況

//1、結(jié)構(gòu)體嵌套結(jié)構(gòu)體
struct Struct3{
    double b;   //8字節(jié)
    int c;      //4字節(jié)
    short d;    //2字節(jié)
    char a;     //1字節(jié)
    struct Struct2 str; 
}Struct3;

//2、打印 Struct3 的內(nèi)存大小
NSLog(@"Struct3內(nèi)存大小:%lu", sizeof(Struct3));
NSLog(@"Struct3中結(jié)構(gòu)體成員內(nèi)存大?。?lu", sizeof(Struct3.str));

打印 的結(jié)果如下:

Struct3內(nèi)存大?。?2
Struct3中結(jié)構(gòu)體成員內(nèi)存大?。?6
  • 分析Struct3的內(nèi)存計(jì)算
    根據(jù)內(nèi)存對齊規(guī)則,來一步一步分析Struct3內(nèi)存大小的計(jì)算過程

    • 變量b:占8個字節(jié),從0開始,此時min(0,8),即0-7 存儲 b
    • 變量c:占4個字節(jié),從8開始,此時min(8,4)8可以整除4,即 8-11存儲c
    • 變量d:占2個字節(jié),從12開始,此時min(12, 2),20可以整除2,即12-13 存儲 d
    • 變量a:占1個字節(jié),從14開始,此時min(14,1),即 14存儲 a
    • 結(jié)構(gòu)體成員str:str是一個結(jié)構(gòu)體,根據(jù)內(nèi)存對齊原則,結(jié)構(gòu)體成員要從其內(nèi)部最大成員大小的整數(shù)倍開始存儲,而Struct2中最大的成員大小為8,所以str要從8的整數(shù)倍開始,當(dāng)前是從15開始,所以不符合要求,需要往后移動到16,168的整數(shù)倍,符合內(nèi)存對齊原則,所以16-31 存儲 str。

因此Struct3的需要的內(nèi)存大小為32字節(jié),而Struct2中最大變量為str, 其內(nèi)存字節(jié)數(shù)為16,所以 Struct2 實(shí)際的內(nèi)存大小必須是 16 的整數(shù)倍,32正好是16的整數(shù)倍,所以 sizeof(Struct3) 的結(jié)果是 32

內(nèi)存優(yōu)化(屬性重排)

Struct1 通過內(nèi)存字節(jié)對齊原則,增加了9個字節(jié),而Struct2通過內(nèi)存字節(jié)對齊原則,通過4+2+1的組合,只需要補(bǔ)齊一個字節(jié)即可滿足字節(jié)對齊規(guī)則,這里得出一個結(jié)論結(jié)構(gòu)體內(nèi)存大小與結(jié)構(gòu)體成員內(nèi)存大小的順序有關(guān)

  • 如果是結(jié)構(gòu)體中數(shù)據(jù)成員是根據(jù)內(nèi)存從小到大的順序定義的,根據(jù)內(nèi)存對齊規(guī)則來計(jì)算結(jié)構(gòu)體內(nèi)存大小,需要增加有較大的內(nèi)存padding即內(nèi)存占位符,才能滿足內(nèi)存對齊規(guī)則,比較浪費(fèi)內(nèi)存

  • 如果是結(jié)構(gòu)體中數(shù)據(jù)成員是根據(jù)內(nèi)存從大到小的順序定義的,根據(jù)內(nèi)存對齊規(guī)則來計(jì)算結(jié)構(gòu)體內(nèi)存大小,我們只需要補(bǔ)齊少量內(nèi)存padding即可滿足內(nèi)存對齊規(guī)則,這種方式就是蘋果中采用的,利用空間換時間,將類中的屬性進(jìn)行重排,來達(dá)到優(yōu)化內(nèi)存的目的

到此,我們在回過頭來看我們一開始演示的Person類,也能驗(yàn)證蘋果中的屬性重排,即內(nèi)存優(yōu)化。

Person
  • 蘋果中針對age、s、d屬性的內(nèi)存進(jìn)行了重排,因?yàn)?code>age類型占4個字節(jié),s和d類型char分別占1個字節(jié),通過4+1+1的方式,按照8字節(jié)對齊,不足補(bǔ)齊的方式存儲在同一塊內(nèi)存中。

總結(jié)

蘋果中的內(nèi)存對齊思想:

  • 大部分的內(nèi)存都是通過固定的內(nèi)存塊進(jìn)行讀取,
  • 盡管我們在內(nèi)存中采用了內(nèi)存對齊的方式,但并不是所有的內(nèi)存都可以進(jìn)行浪費(fèi)的,蘋果會自動對屬性進(jìn)行重排,以此來優(yōu)化內(nèi)存

多少字節(jié)對齊:
我們上篇文章中提及了16字節(jié)對齊,我們在這里提到了8字節(jié)對齊那我們到底采用哪種字節(jié)對齊呢?

  • 對于一個對象來說,其真正的對齊方式 是 8字節(jié)對齊,8字節(jié)對齊已經(jīng)足夠滿足對象的需求了
  • apple系統(tǒng)為了防止一切的容錯,采用的是16字節(jié)對齊的內(nèi)存,主要是因?yàn)椴捎?code>8字節(jié)對齊時,兩個對象的內(nèi)存會緊挨著,顯得比較緊湊,而16字節(jié)比較寬松,利于蘋果以后的擴(kuò)展。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容