在探討內(nèi)存對齊原理之前,首先介紹下iOS中獲取內(nèi)存大小的三種方式
獲取內(nèi)存大小的三種方式
獲取內(nèi)存大小的三種方式分別是:
- sizeof
- class_getInstanceSize
- malloc_size
sizeof
- 1、
sizeof是一個操作符,不是函數(shù) - 2、我們一般用sizeof計算內(nèi)存大小時,
傳入的主要對象是數(shù)據(jù)類型,這個在編譯器的編譯階段(即編譯時)就會確定大小而不是在運(yùn)行時確定。 - 3、
sizeof最終得到的結(jié)果是該數(shù)據(jù)類型占用空間的大小
class_getInstanceSize
這個方法在iOS-底層原理 02:alloc & init & new 源碼分析分析時就已經(jīng)分析了,是runtime提供的api,用于獲取類的實例對象所占用的內(nèi)存大小,并返回具體的字節(jié)數(shù),其本質(zhì)就是獲取實例對象中成員變量的內(nèi)存大小
malloc_size
這個函數(shù)是獲取系統(tǒng)實際分配的內(nèi)存大小
可以通過下面代碼的輸出結(jié)果來驗證我們上面的說法
#import <Foundation/Foundation.h>
#import "LGPerson.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
NSLog(@"objc對象類型占用的內(nèi)存大?。?lu",sizeof(objc));
NSLog(@"objc對象實際占用的內(nèi)存大?。?lu",class_getInstanceSize([objc class]));
NSLog(@"objc對象實際分配的內(nèi)存大小:%lu",malloc_size((__bridge const void*)(objc)));
}
return 0;
}
以下是打印結(jié)果

總結(jié)
-
sizeof:計算類型占用的內(nèi)存大小,其中可以放基本數(shù)據(jù)類型、對象、指針對于類似于
int這樣的基本數(shù)據(jù)而言,sizeof獲取的就是數(shù)據(jù)類型占用的內(nèi)存大小,不同的數(shù)據(jù)類型所占用的內(nèi)存大小是不一樣的而對于類似于NSObject定義的
實例對象而言,其對象類型的本質(zhì)就是一個結(jié)構(gòu)體(即 struct objc_object)的指針,所以sizeof(objc)打印的是對象objc的指針大小,我們知道一個指針的內(nèi)存大小是8,所以sizeof(objc) 打印是 8。注意:這里的8字節(jié)與isa指針一點關(guān)系都沒有?。。。?/p>對于
指針而言,sizeof打印的就是8,因為一個指針的內(nèi)存大小是8,
class_getInstanceSize:計算對象實際占用的內(nèi)存大小,這個需要依據(jù)類的屬性而變化,如果自定義類沒有自定義屬性,僅僅只是繼承自NSObject,則類的實例對象實際占用的內(nèi)存大小是8,可以簡單理解為8字節(jié)對齊malloc_size:計算對象實際分配的內(nèi)存大小,這個是由系統(tǒng)完成的,可以從上面的打印結(jié)果看出,實際分配的和實際占用的內(nèi)存大小并不相等,這個問題可以通過iOS-底層原理 02:alloc & init & new 源碼分析中的16字節(jié)對齊算法來解釋這個問題
結(jié)構(gòu)體內(nèi)存對齊
接下來,我們首先定義兩個結(jié)構(gòu)體,分別計算他們的內(nèi)存大小,以此來引入今天的正題:內(nèi)存對齊原理
//1、定義兩個結(jié)構(gòu)體
struct Mystruct1{
char a; //1字節(jié)
double b; //8字節(jié)
int c; //4字節(jié)
short d; //2字節(jié)
}Mystruct1;
struct Mystruct2{
double b; //8字節(jié)
int c; //4字節(jié)
short d; //2字節(jié)
char a; //1字節(jié)
}Mystruct2;
//計算 結(jié)構(gòu)體占用的內(nèi)存大小
NSLog(@"%lu-%lu",sizeof(Mystruct1),sizeof(Mystruct2));
以下是輸出結(jié)果

從打印結(jié)果我們可以看出一個問題,兩個結(jié)構(gòu)體乍一看,沒什么區(qū)別,其中定義的變量 和 變量類型都是一致的,唯一的區(qū)別只是在于定義變量的順序不一致,那為什么他們做占用的內(nèi)存大小不相等呢?其實這就是iOS中的內(nèi)存字節(jié)對齊現(xiàn)象
內(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)存對齊的原則主要有3點,可以回看iOS-底層原理 02:alloc & init & new 源碼分析中的說明,
可以將內(nèi)存對齊原則可以理解為以下兩點:
- 【原則一】 數(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)前成員的開始位置。 - 【原則二】數(shù)據(jù)成員為結(jié)構(gòu)體:當(dāng)結(jié)構(gòu)體嵌套了結(jié)構(gòu)體時,作為數(shù)據(jù)成員的結(jié)構(gòu)體的
自身長度作為外部結(jié)構(gòu)體的最大成員的內(nèi)存大小,比如結(jié)構(gòu)體a嵌套結(jié)構(gòu)體b,b中有char、int、double等,則b的自身長度為8 - 【原則三】最后
結(jié)構(gòu)體的內(nèi)存大小必須是結(jié)構(gòu)體中最大成員內(nèi)存大小的整數(shù)倍,不足的需要補(bǔ)齊。
驗證對齊規(guī)則
下表是各種數(shù)據(jù)類型在ios中的占用內(nèi)存大小,根據(jù)對應(yīng)類型來計算結(jié)構(gòu)體中內(nèi)存大小

我們可以通過下圖圖來說明下為什么兩個結(jié)構(gòu)體MyStruct1 & MyStruct2的內(nèi)存大小打印不一致的情況,如圖所示

結(jié)構(gòu)體MyStruct1 內(nèi)存大小計算
根據(jù)內(nèi)存對齊規(guī)則計算MyStruct1的內(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
因此MyStruct1的需要的內(nèi)存大小為 15字節(jié),而MyStruct1中最大變量的字節(jié)數(shù)為8,所以 MyStruct1 實際的內(nèi)存大小必須是 8 的整數(shù)倍,18向上取整到24,主要是因為24是8的整數(shù)倍,所以 sizeof(MyStruct1) 的結(jié)果是 24
結(jié)構(gòu)體MyStruct2 內(nèi)存大小計算
根據(jù)內(nèi)存對齊規(guī)則計算MyStruct2的內(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),12可以整除2,即12-13 存儲 d -
變量a:占1個字節(jié),從14開始,此時min(14,1),即14 存儲 a
因此MyStruct2的需要的內(nèi)存大小為 15字節(jié),而MyStruct1中最大變量的字節(jié)數(shù)為8,所以 MyStruct2 實際的內(nèi)存大小必須是 8 的整數(shù)倍,15向上取整到16,主要是因為16是8的整數(shù)倍,所以 sizeof(MyStruct2) 的結(jié)果是 16
結(jié)構(gòu)體嵌套結(jié)構(gòu)體
上面的兩個結(jié)構(gòu)體只是簡單的定義數(shù)據(jù)成員,下面來一個比較復(fù)雜的,結(jié)構(gòu)體中嵌套結(jié)構(gòu)體的內(nèi)存大小計算情況
- 首先定義一個結(jié)構(gòu)體MyStruct3,在MyStruct3中嵌套MyStruct2,如下所示
//1、結(jié)構(gòu)體嵌套結(jié)構(gòu)體
struct Mystruct3{
double b; //8字節(jié)
int c; //4字節(jié)
short d; //2字節(jié)
char a; //1字節(jié)
struct Mystruct2 str;
}Mystruct3;
//2、打印 Mystruct3 的內(nèi)存大小
NSLog(@"Mystruct3內(nèi)存大?。?lu", sizeof(Mystruct3));
NSLog(@"Mystruct3中結(jié)構(gòu)體成員內(nèi)存大?。?lu", sizeof(Mystruct3.str));
打印 的結(jié)果如下所示

-
分析
Mystruct3的內(nèi)存計算
根據(jù)內(nèi)存對齊規(guī)則,來一步一步分析Mystruct3內(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 -
結(jié)構(gòu)體成員str:str是一個結(jié)構(gòu)體,根據(jù)內(nèi)存對齊原則二,結(jié)構(gòu)體成員要從其內(nèi)部最大成員大小的整數(shù)倍開始存儲,而MyStruct2中最大的成員大小為8,所以str要從8的整數(shù)倍開始,當(dāng)前是從15開始,所以不符合要求,需要往后移動到16,16是8的整數(shù)倍,符合內(nèi)存對齊原則,所以16-31 存儲 str
-
因此MyStruct3的需要的內(nèi)存大小為 32字節(jié),而MyStruct3中最大變量為str, 其最大成員內(nèi)存字節(jié)數(shù)為8,根據(jù)內(nèi)存對齊原則,所以 MyStruct3 實際的內(nèi)存大小必須是 8 的整數(shù)倍,32正好是8的整數(shù)倍,所以 sizeof(MyStruct3) 的結(jié)果是 32
其內(nèi)存存儲情況如下圖所示

二次驗證
為了保險起見,我們再定義一個結(jié)構(gòu)體,來驗證我們結(jié)構(gòu)體嵌套的內(nèi)存大小計算說明
struct Mystruct4{
int a; //4字節(jié) min(0,4)--- (0,1,2,3)
struct Mystruct5{ //從4開始,存儲開始位置必須是最大的整數(shù)倍(最大成員為8),min(4,8)不符合 4,5,6,7,8 -- min(8,8)滿足,從8開始存儲
double b; //8字節(jié) min(8,8) --- (8,9,10,11,12,13,14,15)
short c; //1字節(jié),從16開始,min(16,1) -- (16,17)
}Mystruct5;
}Mystruct4;
分析如下
-
變量a:占4字節(jié),從0開始,min(0,4),即0-3存儲a -
結(jié)構(gòu)體Mystruct5:從4開始,根據(jù)內(nèi)存對齊原則二,即存儲開始位置必須是最大的整數(shù)倍(最大成員為8),min(4,8)不能整除,繼續(xù)往后移動,直到8, min(8,8)滿足,從8開始存儲結(jié)構(gòu)體Mystruct5的變量-
變量b:占8字節(jié),從8開始,min(8,8),可以整除,即8-15存儲b -
變量c:占2字節(jié),從16開始,min(16,2),可以整除,即16-17存儲c
-
因此Mystruct4中需要的內(nèi)存大小是 18字節(jié),根據(jù)內(nèi)存對其原則二,Mystruct4實際的內(nèi)存大小必須是Mystruct5中最大成員b的整數(shù)倍,即必須是8的整數(shù)倍,所以sizeof(Mystruct4) 的結(jié)果是 24
以下是運(yùn)行結(jié)果的打印,以此來印證24這個內(nèi)存大小

內(nèi)存優(yōu)化(屬性重排)
MyStruct1 通過內(nèi)存字節(jié)對齊原則,增加了9個字節(jié),而MyStruct2通過內(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ī)則來計算結(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ī)則來計算結(jié)構(gòu)體內(nèi)存大小,我們只需要補(bǔ)齊少量內(nèi)存padding即可滿足堆存對齊規(guī)則,這種方式就是蘋果中采用的,利用空間換時間,將類中的屬性進(jìn)行重排,來達(dá)到優(yōu)化內(nèi)存的目的
以下面這個例子來進(jìn)行說明 蘋果中屬性重排,即內(nèi)存優(yōu)化
- 定義一個自定義CJLPerson類,并定義幾個屬性,
@interface CJLPerson : 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 CJLPerson
@end
- 在main中創(chuàng)建CJLPerson的實例對象,并對其中的幾個屬性賦值
int main(int argc, char * argv[]) {
@autoreleasepool {
CJLPerson *person = [CJLPerson alloc];
person.name = @"CJL";
person.nickName = @"C";
person.age = 18;
person.c1 = 'a';
person.c2 = 'b';
NSLog(@"%@",person);
}
return 0;
}
- 斷點調(diào)試person,根據(jù)
CJLPerson的對象地址,查找出屬性的值-
通過地址找出
name & nickName
image -
當(dāng)我們向通過
0x0000001200006261地址找出age等數(shù)據(jù)時,發(fā)現(xiàn)是亂碼,這里無法找出值的原因是蘋果中針對age、c1、c2屬性的內(nèi)存進(jìn)行了重排,因為age類型占4個字節(jié),c1和c2類型char分別占1個字節(jié),通過4+1+1的方式,按照8字節(jié)對齊,不足補(bǔ)齊的方式存儲在同一塊內(nèi)存中,- age的讀取通過
0x00000012 - c1的讀取通過
0x61(a的ASCII碼是97) - c2的讀取通過
0x62(b的ASCII碼是98)
image
- age的讀取通過
-
下圖是CJLPerson的內(nèi)存分布情況

注意:
1、char類型的數(shù)據(jù)讀取出來是以ASCII碼的形式顯示
2、圖片中地址為0x0000000000000000,表示person中還有屬性未賦值
總結(jié)
所以,這里可以總結(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é)對齊到底采用多少字節(jié)對齊?
到目前為止,我們在前文既提到了8字節(jié)對齊,也提及了16字節(jié)對齊,那我們到底采用哪種字節(jié)對齊呢?
我們可以通過objc4中class_getInstanceSize的源碼來進(jìn)行分析
/**
* 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字節(jié)對齊
return (x + WORD_MASK) & ~WORD_MASK;
}
//其中 WORD_MASK 為
# define WORD_MASK 7UL
通過源碼可知:
- 對于一個
對象來說,其真正的對齊方式是8字節(jié)對齊,8字節(jié)對齊已經(jīng)足夠滿足對象的需求了 - apple系統(tǒng)為了防止一切的容錯,采用的是16字節(jié)對齊的內(nèi)存,主要是因為采用8字節(jié)對齊時,兩個對象的內(nèi)存會緊挨著,顯得比較緊湊,而16字節(jié)比較寬松,利于蘋果以后的擴(kuò)展。
總結(jié)
綜合前文提及的獲取內(nèi)存大小的方式
-
class_getInstanceSize:是采用8字節(jié)對齊,參照的對象的屬性內(nèi)存大小 -
malloc_size:采用16字節(jié)對齊,參照的整個對象的內(nèi)存大小,對象實際分配的內(nèi)存大小必須是16的整數(shù)倍
內(nèi)存對齊算法
目前已知的16字節(jié)內(nèi)存對齊算法有兩種
-
alloc源碼分析中的align16: -
malloc源碼分析中的segregated_size_to_fit
align16: 16字節(jié)對齊算法
這個算法的思想已經(jīng)在iOS-底層原理 02:alloc & init & new 源碼分析中有所提及
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
segregated_size_to_fit: 16字節(jié)對齊算法
#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
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
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相當(dāng)于將后4位抹零,跟 k/16 * 16一樣 ,是16字節(jié)對齊算法,小于16就成0了
以 k = 2為例,如下圖所示


