一、內(nèi)存對齊
獲取內(nèi)存大小的三種方式分別是:
- sizeof:sizeof計算內(nèi)存大小時,
傳入的主要對象是數(shù)據(jù)類型,這個在編譯器的編譯階段(即編譯時)就會確定大小而不是在運行時確定。最終得到的結(jié)果是該數(shù)據(jù)類型占用空間的大小。 - class_getInstanceSize:用于
獲取類的實例對象所占用的內(nèi)存大小,并返回具體的字節(jié)數(shù),其本質(zhì)就是獲取實例對象中成員變量的內(nèi)存大小 - malloc_size:這個函數(shù)是
獲取系統(tǒng)實際分配的內(nèi)存大小
extern size_t malloc_size(const void *ptr);/* Returns size of given ptr */
/**
* 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);
通過下面例子驗證一下:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
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打印出來8:因為objc是NSObject定義的實例對象,實例對象的本質(zhì)是一個結(jié)構(gòu)體指針,指針類型占用的空間是8字節(jié)。 -
class_getInstanceSize打印出來的也是8:因為objc對象沒有屬性,只有指針占用8字節(jié),所以占用的真實內(nèi)存大小是8字節(jié)。 -
malloc_size:計算機實際分配的內(nèi)存大小是16字節(jié)。因為16字節(jié)對齊,在上一篇研究alloc源碼時已經(jīng)了解過。
結(jié)構(gòu)體內(nèi)存對齊
那么蘋果是怎么計算一個對象占用多少字節(jié)的?也就是內(nèi)存對齊究竟是怎么做的。
內(nèi)存對齊的原則:
1、數(shù)據(jù)成員對?規(guī)則:結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,第一個數(shù)據(jù)成員放在offset為0的地方,以后每個數(shù)據(jù)成員存儲的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員,比如說是數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開始(比如int為4字節(jié),則要從4的整數(shù)倍地址開始存儲)。
2、結(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ù)倍開始存儲.)
3、收尾工作:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍,不足的要補?
另外一種更加形象的描述:
- 【原則一】 數(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ù)倍,不足的需要補齊。
例子一:結(jié)構(gòu)體占用內(nèi)存對比
#import <Foundation/Foundation.h>
//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;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//計算 結(jié)構(gòu)體占用的內(nèi)存大小
NSLog(@"%lu-%lu",sizeof(Mystruct1),sizeof(Mystruct2));
}
return 0;
}
運行的結(jié)果如下:

分析原因:根據(jù)內(nèi)存對齊規(guī)則我們模擬兩個結(jié)構(gòu)體的內(nèi)存對齊過程:

Mystruct1的內(nèi)存對齊過程如上圖所示,Mystruct2的過程如下:
根據(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é),而MyStruct2中最大變量的字節(jié)數(shù)為8,所以 MyStruct2 實際的內(nèi)存大小必須是 8 的整數(shù)倍,15向上取整到16,主要是因為16是8的整數(shù)倍,所以 sizeof(MyStruct2) 的結(jié)果是 16
例子二:結(jié)構(gòu)體嵌套結(jié)構(gòu)體
代碼如下:
//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é)果:

根據(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),12可以整除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
例子三:結(jié)構(gòu)體嵌套結(jié)構(gòu)體二次驗證
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; //2字節(jié),從16開始,min(16,2) -- (16,17)
}Mystruct5;
/*結(jié)構(gòu)體Mystruct5結(jié)束,按照【原則三】`結(jié)構(gòu)體的內(nèi)存大小`必須是結(jié)構(gòu)體中`最大成員內(nèi)存大小`的整數(shù)倍,不足的需要補齊。
當(dāng)前結(jié)構(gòu)體長度為10,補充6字節(jié)。(18,19,20,21,22,23)留空。
*/
short d; //2字節(jié),offset從24開始,能整除。 (24,25)存放
}Mystruct4;
/*結(jié)構(gòu)體結(jié)束,按照【原則三】`結(jié)構(gòu)體的內(nèi)存大小`必須是結(jié)構(gòu)體中`最大成員內(nèi)存大小`的整數(shù)倍,不足的需要補齊。
當(dāng)前結(jié)構(gòu)體長度為26,補充6字節(jié)。(26,27,28,29,30,31)留空。
*/
NSLog(@"Mystruct4內(nèi)存大?。?lu", sizeof(Mystruct4));
NSLog(@"Mystruct5內(nèi)存大小:%lu", sizeof(Mystruct4.Mystruct5));

二、內(nèi)存優(yōu)化(屬性重排)
我們知道對象在底層就是一個結(jié)構(gòu)體對象。從上面結(jié)構(gòu)體內(nèi)存對齊的內(nèi)容,得出結(jié)構(gòu)體占用內(nèi)存大小跟結(jié)構(gòu)體中成員變量的順序有關(guān)。那么對象的內(nèi)存大小是否跟成員變量的順序有關(guān)呢?
思考:如果是的話,那么我們平時寫代碼的時候,豈不是要時時刻刻注意,成員變量的順序,所以答案肯定是否定的。蘋果系統(tǒng)底層會幫我們自動進(jìn)行內(nèi)存優(yōu)化。
對象雖然會以結(jié)構(gòu)體的形式進(jìn)行存儲,但是在保存時,蘋果會計算出最優(yōu)的存儲順序,達(dá)到減少內(nèi)存消耗的目的。
例一:驗證內(nèi)存優(yōu)化
新建兩個類,Man和Women,分別添加兩個int和一個long。將順序打亂,如果純粹按照結(jié)構(gòu)體內(nèi)存對齊,它將分別是16字節(jié)和24字節(jié)。代碼如下:
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface Man : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int wight;
@property (nonatomic, assign) long height;
@end
@implementation Man
@end
@interface Woman : NSObject
@property (nonatomic, assign) int wight;
@property (nonatomic, assign) long height;
@property (nonatomic, assign) int age;
@end
@implementation Woman
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Man *man = [[Man alloc] init];
man.age = 18;
man.height = 185;
man.wight = 75;
Woman *woman = [[Woman alloc] init];
woman.age = 18;
woman.height = 185;
woman.wight = 75;
NSLog(@"man對象類型占用的內(nèi)存大?。?lu,woman對象類型占用的內(nèi)存大?。?lu",sizeof(man),sizeof(woman));
NSLog(@"man對象實際占用的內(nèi)存大?。?lu,woman對象實際占用的內(nèi)存大?。?lu",class_getInstanceSize([man class]),class_getInstanceSize([woman class]));
NSLog(@"man對象實際分配的內(nèi)存大?。?lu,woman對象實際分配的內(nèi)存大?。?lu",malloc_size((__bridge const void*)(man)),malloc_size((__bridge const void*)(woman)));
}
return 0;
}

結(jié)果:類對象是指針,占8字節(jié)。一個int占4字節(jié),兩個int占8字節(jié),一個long占8字節(jié),加上指針占8字節(jié),實際一共24字節(jié)。對象類型需要16字節(jié)對齊,必須是16的倍數(shù),所以對象占用32字節(jié)。
分析:按照結(jié)構(gòu)體內(nèi)存對齊,Man和Woman類所占的內(nèi)存空間肯定不一樣,可結(jié)果確是完全一模一樣,說明了蘋果在類轉(zhuǎn)換成結(jié)構(gòu)體存儲時,做了內(nèi)存優(yōu)化。
通過打印內(nèi)存二次驗證

分析結(jié)果:第一個字節(jié)是ISA,第二個字節(jié)順序不同了,第三個8字節(jié)一模一樣,第四字節(jié)都是補充0。第二8字節(jié)0x12和0x4b順序不同,正是蘋果對內(nèi)存做的優(yōu)化??梢圆乱幌?,蘋果先是忽略了long,按int的順序補齊。真正要研究它是怎么優(yōu)化的過程,需要去看LLVM的處理邏輯,在加載類時的邏輯。
關(guān)于字節(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é)比較寬松,利于蘋果以后的擴展。
目前已知的16字節(jié)內(nèi)存對齊算法有兩種
-
alloc源碼分析中的align16: -
malloc源碼分析中的segregated_size_to_fit
align16: 16字節(jié)對齊算法
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;
}
三、改變編譯器字節(jié)對齊方式
在一些與用硬件設(shè)備交互的場景中,比如C語言寫的加密算法,可能需要直接iOS和安卓端按照設(shè)備端C語言編譯器的處理方式,而不是通常情況下Xcode的編譯處理方式。因為字節(jié)對齊的問題,加密位數(shù)的不一致,會導(dǎo)致校驗失敗。在安卓端可以通過JNI實現(xiàn)java和C兩個編譯環(huán)境,不需要處理這種問題。但是在iOS端,雖然是可以直接運行C語言,但卻和極其在意內(nèi)存使用的嵌入式設(shè)備端C語言編譯器有區(qū)別。請看如下代碼,還是上面的Mystruct1和Mystruct2:

分析結(jié)果:加了#pragma pack(2)之后,根本不理會所謂的字節(jié)對齊的內(nèi)存存儲方式。加了#pragma pack之后,就可以實現(xiàn)完全跟各種編譯器匹配,直接使用C語言完成加密算法的校驗。簡直不要太爽。
#pragma pack的作用
程序編譯器對結(jié)構(gòu)的存儲的特殊處理確實提高CPU存儲變量的速度,但是有時候也帶來了一些麻煩,我們也屏蔽掉變量默認(rèn)的對齊方式,自己可以設(shè)定變量的對齊方式。
編譯器中提供了#pragma pack(n)來設(shè)定變量以n字節(jié)對齊方式。n字節(jié)對齊就是說變量存放的起始地址的偏移量有兩種情況:
第一、如果n大于等于該變量所占用的字節(jié)數(shù),那么偏移量必須滿足默認(rèn)的對齊方式,
第二、如果n小于該變量的類型所占用的字節(jié)數(shù),那么偏移量為n的倍數(shù),不用滿足默認(rèn)的對齊方式。
結(jié)構(gòu)的總大小也有個約束條件,分下面兩種情況:如果n大于所有成員變量類型所占用的字節(jié)數(shù),那么結(jié)構(gòu)的總大小必須為占用空間最大的變量占用的空間數(shù)的倍數(shù);否則必須為n的倍數(shù)。