上篇文章中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類型age和height共用一個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é)果

這時候我們發(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(通常它為4或8的倍數(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ù)的吞吐量 -
64位CPU一次處理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),n從m位置開始存儲, 反之繼續(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í)踐來分析。
結(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)體
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,16是8的整數(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)化。

- 蘋果中針對
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ò)展。




