前言
在iOS原理 OC對象的實例化一文中介紹了alloc的底層調(diào)用流程应闯,在其核心步驟instanceSize
方法里疲迂,完成了對象的內(nèi)存大小計算沸伏,返回的size即為創(chuàng)建對象需要申請的內(nèi)存空間大小须尚,本文將詳細介紹系統(tǒng)是如何計算對象內(nèi)存空間的大小。
一假消、instanceSize方法的底層實現(xiàn)
首先需要知道柠并,對象的內(nèi)存空間里存放的是對象的屬性,所以計算內(nèi)存大小即為計算所有屬性所占的內(nèi)存大小富拗,先看下objc源碼的底層實現(xiàn):
size_t instanceSize(size_t extraBytes) const {
//方式一:編譯器快速計算內(nèi)存大芯视琛(16字節(jié)對齊)
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
//方式二:計算類中所有屬性和方法的內(nèi)存占用 + 額外的字節(jié)數(shù)0(8字節(jié)對齊)
size_t size = alignedInstanceSize() + extraBytes;
//CF requires all objects be at least 16 bytes.
//最少申請16字節(jié)的內(nèi)存大小
if (size < 16) size = 16;
return size;
}
上面兩個方式的區(qū)別,在于將對象所有屬性所占的內(nèi)存大小采取不同的對齊方式媒峡,方式一是16字節(jié)對齊瘟栖,方式二是8字節(jié)對齊。
注意谅阿,在不同版本的objc源碼中半哟,instanceSize
方法的底層實現(xiàn)可能是不一樣的,本文參照的是最新的objc-781
源碼签餐,里面會有這兩種計算方式寓涨,但是實際運行,其實是以方式一來計算的氯檐,16字節(jié)內(nèi)存對齊戒良。而在老版本的objc源碼中,比如objc-750
源碼冠摄,里面只有方式二的計算方式糯崎,采取的是8字節(jié)對齊。
//objc-750源碼中的instanceSize方法
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
雖然這個方法的底層實現(xiàn)不會頻繁更新河泳,但大家以后在分析時沃呢,最好是參照最新的objc源碼進行分析。話不多說拆挥,接下來分析這兩種方式具體是怎么計算的薄霜。
1.1 十六字節(jié)對齊 - fastInstanceSize
從源碼中可知,是在fastInstanceSize
方法里完成內(nèi)存計算的:
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
編譯器快速計算出需要的size后纸兔,最后會執(zhí)行align16
方法片橡,即『16字節(jié)對齊』盆偿。
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
系統(tǒng)在計算出內(nèi)存size后伯铣,會先加15阵苇,再與個非15,這樣就實現(xiàn)了16字節(jié)對齊洲拇。接下來通過兩組二進制運算來直觀地看看字節(jié)對齊后的結(jié)果:
//已知: 15的二進制為: 0000 1111
// ~15的二進制為:1111 0000
//假設(shè)計算出當前類需要的內(nèi)存大小為12字節(jié)流强,則16字節(jié)對齊的計算過程如下:
0000 1100 // 12字節(jié)
0000 1111 // +15
0001 1011 // = 27
1111 0000 // ~15
0001 0000 // 27 & (~15) = 16
//假設(shè)計算出當前類需要的內(nèi)存大小為24字節(jié)痹届,則16字節(jié)對齊的計算過程如下:
0001 1000 // 24字節(jié)
0000 1111 // +15
0010 0111 // = 39
1111 0000 // ~15
0010 0000 // 39 & (~15) = 32
從上面兩組二進制運算得知呻待,12字節(jié)打月、24字節(jié)的內(nèi)存經(jīng)由16字節(jié)對齊后,實際申請內(nèi)存大小分別為16字節(jié)蚕捉、32字節(jié)奏篙。因此,不管對象屬性實際需要多大的內(nèi)存空間迫淹,在新版本中秘通,系統(tǒng)都會以16字節(jié)的倍數(shù)來申請內(nèi)存空間,并且最少16字節(jié)敛熬,這就是『16字節(jié)對齊』肺稀。
1.2 八字節(jié)對齊 - alignedInstanceSize
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
在實例化對象時,經(jīng)由斷點調(diào)試可知应民,extraBytes為0话原,所以內(nèi)存大小依賴于alignedInstanceSize
方法計算:
uint32_t alignedInstanceSize() {
//step1:通過unalignedInstanceSize方法獲取到需要的內(nèi)存大小
//step2:通過word_align方法對內(nèi)存大小進行8字節(jié)運算
return word_align(unalignedInstanceSize());
}
可以看到,class_getInstanceSize方法最終實現(xiàn)就兩步诲锹,先獲取到對象屬性所占的內(nèi)存大小繁仁,再將內(nèi)存大小進行8字節(jié)對齊運算。
- unalignedInstanceSize
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
這一步獲取到的是對象的屬性所占的內(nèi)存大小归园,ro是個很重要的知識點黄虱,會在后續(xù)介紹類和對象的結(jié)構(gòu)時作詳細講解,本篇文章就不作過多拓展了庸诱。
- word_align
#define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
WORD_MASK的值為7捻浦,這里其實是對內(nèi)存大小作8字節(jié)對齊運算。對齊計算和上文的16進制對齊一樣桥爽,大家可以自己進行二進制演算朱灿,這里就不再推演了。在完成8字節(jié)對齊運算后聚谁,還會再判斷size的大小母剥,如果小于16,就申請16字節(jié)的內(nèi)存形导。 也就是說环疼,不管對象的屬性實際需要多大的內(nèi)存,在老版本中朵耕,系統(tǒng)都會以8字節(jié)的倍數(shù)來申請內(nèi)存空間炫隶,并且最少16字節(jié)。
二阎曹、對內(nèi)存進行字節(jié)對齊的原因
理解了內(nèi)存對齊邏輯后伪阶,可能會有疑問煞檩,為什么要對內(nèi)存進行字節(jié)對齊呢?為什么不按對象屬性實際占用的內(nèi)存大小來分配內(nèi)存呢栅贴?經(jīng)由字節(jié)對齊后斟湃,分配的空間要比實際需要的多,這樣會不會造成內(nèi)存浪費檐薯?
在解答這些疑問前凝赛,我們需要先清楚,對象里可能會有多種數(shù)據(jù)類型的屬性坛缕,這些屬性占用內(nèi)存大小是不一樣的墓猎,如果直接按照實際需要內(nèi)存進行分配,則cpu在讀取內(nèi)存數(shù)據(jù)時需要先知道每個數(shù)據(jù)占用多大內(nèi)存赚楚,這樣才能保證讀取的數(shù)據(jù)是完整的毙沾,讀取效率較低。
下面這個表格是各種類型數(shù)據(jù)分別在32位和64位系統(tǒng)中占用內(nèi)存的大小宠页,單位:字節(jié)左胞。(注:本文都是基于64位系統(tǒng)分析的)
C | OC | 32位 | 64位 |
---|---|---|---|
bool | BooL(64位) | 1 | 1 |
signed char | (_ _signed char)int8_t、BOOL(32位) | 1 | 1 |
unsigned char | Boolean | 1 | 1 |
short | int16_t | 2 | 2 |
unsigned short | unichar | 2 | 2 |
int勇皇、int32_t | NSInteger(32位)罩句、boolean_ t(32位) | 4 | 4 |
unsigned int | boolean_ t(64位)、NSUInteger(32位) | 4 | 4 |
long | NSInteger(64位) | 4 | 8 |
unsigned long | NSUlnteger(64位) | 4 | 8 |
long long | int64_t | 8 | 8 |
float | CGFloat(32位) | 4 | 4 |
double | CGFloat(64位) | 8 | 8 |
從表格里清晰的看到敛摘,屬性最多占用8字節(jié)內(nèi)存门烂。而對象里最常見的屬性是指針類型,指針類型的數(shù)據(jù)在內(nèi)存里也是占用8字節(jié)的大小兄淫。這樣屯远,以8字節(jié)的倍數(shù)來開辟內(nèi)存空間,CPU再以8字節(jié)為一段進行讀取捕虽,既能保證每個數(shù)據(jù)讀取的完整性慨丐,又能提高讀取效率,這就是最初系統(tǒng)8字節(jié)對齊的原因泄私。
舉個例子房揭,創(chuàng)建一個Person對象,對象里有多種數(shù)據(jù)類型的屬性晌端,來看看字節(jié)對齊對分配內(nèi)存的影響捅暴。(注:這里只是看下字節(jié)對齊的影響,就先不考慮isa指針了)
@interface Person : NSObject
@property (nonatomic, strong) NSString *name; //8
@property (nonatomic, strong) NSString *sex; //8
@property (nonatomic, assign) int age; //4
@property (nonatomic, assign) long height; //8
@property (nonatomic) char c1; //1
@property (nonatomic) char c2; //1
@end
如果不作字節(jié)對齊咧纠,那么系統(tǒng)分配內(nèi)存會是下面的情況:
系統(tǒng)如果以屬性實際占用內(nèi)存大小來分配蓬痒,雖然有效利用了內(nèi)存空間,但是在讀取數(shù)據(jù)時漆羔,CPU就需要預(yù)先知道每個屬性占用的內(nèi)存大小梧奢,并按對應(yīng)字節(jié)長度讀取讀取內(nèi)存狱掂,否則讀取的數(shù)據(jù)就不完整。
比如亲轨,在上圖中趋惨,若讀取age數(shù)據(jù),如果此時CPU不按照4字節(jié)長度來讀取瓶埋,要么沒讀取到完整的age數(shù)據(jù)希柿,要么會越界讀到height的數(shù)據(jù)。
因此养筒,系統(tǒng)如果以屬性實際占用內(nèi)存大小來分配,CPU讀取內(nèi)存數(shù)據(jù)時既不安全端姚,效率也低晕粪。
所以系統(tǒng)會對內(nèi)存進行字節(jié)對齊,內(nèi)存分配情況大致如下(注:不是一定按照這個順序的):
若進行字節(jié)對齊渐裸,在分配內(nèi)存時巫湘,會進行屬性重排,將age昏鹃、c1尚氛、c2(總共6字節(jié))這三個屬性存在一個8字節(jié)段中,在讀取數(shù)據(jù)時洞渤,CPU也是按照8字節(jié)為一段來讀取阅嘶,這樣既能保證讀取數(shù)據(jù)的完整性,又能提高讀取效率载迄。雖說會多開辟幾字節(jié)的內(nèi)存空間讯柔,但是保證了讀取安全,提高了讀取效率护昧,完全是值得的魂迄。而且鑒于現(xiàn)在設(shè)備內(nèi)存越來越大,這種『以空間換時間』來提高效率的方式惋耙,會越來越常見捣炬。
三、計算對象申請的內(nèi)存空間大小
內(nèi)存空間里保存的是對象的屬性绽榛,所以計算內(nèi)存空間大小即是統(tǒng)計對象所有屬性所占的內(nèi)存大小湿酸。需要注意的是,對象都是繼承自NSObject的蒜田,NSObject里有個isa成員屬性稿械,是指針類型,需要占用8字節(jié)內(nèi)存冲粤。
//NSObjct的結(jié)構(gòu)
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
isa的具體用途后面會講解美莫,本文不再拓展页眯。在接下來的內(nèi)存計算中,都需要加上其占用的8字節(jié)內(nèi)存厢呵。
按照前面的思路窝撵,計算對象實例化時申請的內(nèi)存空間大小,即先統(tǒng)計所有屬性所占的內(nèi)存大小襟铭,然后再進行16字節(jié)對齊就可得到碌奉。那怎么印證這個計算規(guī)則呢?最直觀的方法就是獲取并打印對象的內(nèi)存size寒砖,之前有看過其他文章赐劣,很多都是通過class_getInstanceSize
方法來獲取的,先來看下這個方法的實現(xiàn)邏輯:
size_t class_getInstanceSize(Class cls){
if (!cls) return 0;
return cls->alignedInstanceSize();
}
可以看到哩都,class_getInstanceSize
方法里面調(diào)用的是alignedInstanceSize
方法魁兼,就回到了上文1.2里面的八字節(jié)對齊處理了,所以如果是老版本漠嵌,就可以正常使用咐汞,對于新版本的16字節(jié)對齊就不適用了。在新版本里可以改寫這個方法的對齊運算儒鹿,然后再使用化撕,比如這樣:
//將8字節(jié)對齊改成16字節(jié)對齊
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
// return cls->alignedInstanceSize();
return cls->instanceSize(0);
}
但為了更直觀的區(qū)分8字節(jié)和16字節(jié)的對齊結(jié)果,這里我選擇在class_createInstanceFromZone
方法里打印需要申請的內(nèi)存大小约炎。
- 先在objc-runtime-new.h文件里添加一個老版本8字節(jié)對齊的方法植阴。
//這也是objc-750版本的instanceSize方法
size_t oldInstanceSize(size_t extraBytes) const {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
- 然后在
class_createInstanceFromZone
方法里再獲取一個8字節(jié)對齊的結(jié)果,并將兩種對齊方式的內(nèi)存大小打印出來章钾。
//這是原本的16字節(jié)對齊計算
size_t size;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
//這里再獲取一個8字節(jié)對齊結(jié)果
size_t size8;
size8 = cls->oldInstanceSize(extraBytes);
//打印兩個結(jié)果
printf("size16 = %lu \nsize8 = %lu\n", size, size8);
- 最后實例化Person對象墙贱,打印結(jié)果。為了對比更加直觀贱傀,也打印出通過
class_getInstanceSize
方法獲取到的內(nèi)存大小惨撇。
準備工作完成,那接下來分析在各種情況下創(chuàng)建Person對象需要申請的內(nèi)存空間大小府寒。
3.1 不含任何自定義屬性的Person對象
//不含自定義屬性的Person對象
@interface Person : NSObject
@end
//創(chuàng)建Person對象魁衙,打印內(nèi)存大小
Person *person = [[Person alloc] init];
NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class]));
//打印結(jié)果
size16 = 16 //16字節(jié)對齊的結(jié)果
size8 = 16 //8字節(jié)對齊的結(jié)果
class_getInstanceSize = 8 //獲取的8字節(jié)對齊的內(nèi)存
person對象里只有一個isa屬性,占8字節(jié)株搔,那屬性總共占用8字節(jié)的內(nèi)存大衅实怼:
-
size16 = 16
:經(jīng)由16字節(jié)對齊后,結(jié)果為16字節(jié)纤房。 -
size8 = 16
:經(jīng)由8字節(jié)對齊后纵隔,結(jié)果為8字節(jié)。但oldInstanceSize方法最后對內(nèi)存大小進行了判斷,如果小于16捌刮,就返回16碰煌。 -
class_getInstanceSize = 8
:這個方法只作了8字節(jié)對齊,所以返回為8绅作。
3.2 自定義指針類型屬性的Person對象
//聲明兩個指針類型的屬性
@interface Person : NSObject
@property (nonatomic, strong) NSString *name; //8
@property (nonatomic, strong) NSString *sex; //8
@end
//創(chuàng)建Person對象芦圾,打印內(nèi)存大小
Person *person = [[Person alloc] init];
NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class]));
//打印結(jié)果
size16 = 32 //16字節(jié)對齊的結(jié)果
size8 = 24 //8字節(jié)對齊的結(jié)果
class_getInstanceSize = 24 //獲取的8字節(jié)對齊的內(nèi)存
person對象里有三個指針類型的數(shù)據(jù)(isa,name俄认,sex)个少,屬性總共占用24字節(jié)的內(nèi)存大小:
-
size16 = 32
:經(jīng)由16字節(jié)對齊后眯杏,結(jié)果為32字節(jié)夜焦。 -
size8 = 24
:經(jīng)由8字節(jié)對齊后,結(jié)果為24字節(jié)役拴。 -
class_getInstanceSize = 24
:這個方法只作了8字節(jié)對齊糊探,所以返回為24。
3.3 自定義多種數(shù)據(jù)類型屬性的Person對象
//Person類里自定義了多種類型的屬性
@interface Person : NSObject
@property (nonatomic, strong) NSString *name; //8
@property (nonatomic, strong) NSString *sex; //8
@property (nonatomic, assign) int age; //4
@property (nonatomic, assign) long height; //8
@property (nonatomic) char c1; //1
@property (nonatomic) char c2; //1
@end
//創(chuàng)建Person對象河闰,打印內(nèi)存大小
Person *person = [[Person alloc] init];
NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class]));
//打印結(jié)果
size16 = 48 //16字節(jié)對齊的結(jié)果
size8 = 40 //8字節(jié)對齊的結(jié)果
class_getInstanceSize = 40 //獲取的8字節(jié)對齊的內(nèi)存
person對象里的屬性總共占用38字節(jié)的內(nèi)存大小:
-
size16 = 48
:經(jīng)由16字節(jié)對齊后褥紫,結(jié)果為48字節(jié)姜性。 -
size8 = 40
:經(jīng)由8字節(jié)對齊后,結(jié)果為40字節(jié)髓考。 -
class_getInstanceSize = 40
:這個方法只作了8字節(jié)對齊部念,所以返回為40。
3.4 對象里包含結(jié)構(gòu)體成員屬性
在Person對象里添加結(jié)構(gòu)體成員氨菇,再看下打印結(jié)果儡炼。
//聲明一個結(jié)構(gòu)體struct1,根據(jù)前面的分析可知查蓉,內(nèi)存為24乌询。
struct Struct1 {
double a; // 8
char b; // 1
int c; // 4
short d; // 2
};
//在person最后面添加一個struct1的類
@interface Person : NSObject
@property (nonatomic, strong) NSString *name; //8
@property (nonatomic, assign) int age; //4
@property (nonatomic, assign) long height; //8
@property (nonatomic) char c1; //1
@property (nonatomic) struct Struct1 str; //24
@end
//創(chuàng)建Person對象,打印內(nèi)存大小
Person *person = [[Person alloc] init];
NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class]));
//打印結(jié)果
size16 = 64 //16字節(jié)對齊的結(jié)果
size8 = 56 //8字節(jié)對齊的結(jié)果
class_getInstanceSize = 56 //獲取的8字節(jié)對齊的內(nèi)存
person對象其他屬性占用了29個字節(jié)豌研,在對象內(nèi)存里存放在位置0-28 妹田。結(jié)構(gòu)體str成員,本應(yīng)該在位置29處開始存放鹃共,但根據(jù)結(jié)構(gòu)體內(nèi)存對齊規(guī)則鬼佣,當結(jié)構(gòu)體作為成員時,則結(jié)構(gòu)體成員要從其內(nèi)部最?元素所占內(nèi)存??的整數(shù)倍地址開始存儲霜浴,結(jié)構(gòu)體str里最大成員占用8字節(jié)內(nèi)存晶衷,所以str需要從8的整數(shù)倍開始存儲,即從位置32開始存放,存放位置為32-55晌纫,所以最后person對象需要56字節(jié)內(nèi)存:
-
size16 = 64
:經(jīng)由16字節(jié)對齊后税迷,結(jié)果為64字節(jié)。 -
size8 = 56
:經(jīng)由8字節(jié)對齊后缸匪,結(jié)果為56字節(jié)翁狐。 -
class_getInstanceSize = 56
:這個方法只作了8字節(jié)對齊,所以返回為56凌蔬。
通過分析上述示例的打印結(jié)果露懒,均可印證前面說的內(nèi)存大小的計算規(guī)則:先統(tǒng)計所有屬性所占的內(nèi)存大小,然后再進行16字節(jié)對齊砂心。簡單數(shù)據(jù)類型的屬性所占內(nèi)存比較容易統(tǒng)計懈词,但對于比較復(fù)雜的數(shù)據(jù)結(jié)構(gòu),比如說結(jié)構(gòu)體(struct
)辩诞,它的內(nèi)存計算就需要遵循『結(jié)構(gòu)體內(nèi)存對齊規(guī)則』坎弯。
四、sizeof & class_getInstanceSize & malloc_size
在打印內(nèi)存大小時译暂,用到了這三個方法:
-
sizeof
:是一個運算符抠忘,用來計算傳進來的數(shù)據(jù)類型占用多大的內(nèi)存,在編譯時即可完成運算外永。 -
class_getInstanceSize
:獲取對象實例化時需要申請的內(nèi)存大小崎脉,采取的是8字節(jié)對齊方式,調(diào)用時需要先引用#import <objc/runtime.h>
伯顶。 -
malloc_size
:獲取對象實例化時系統(tǒng)實際開辟的內(nèi)存大小囚灼,采取的是16字節(jié)對齊方式,調(diào)用時需要先引用#import <malloc/malloc.h>
祭衩。
五灶体、總結(jié)
對象的內(nèi)存大小依賴其包含的屬性,所以在實例化對象時掐暮,系統(tǒng)會先統(tǒng)計對象所有屬性所占的內(nèi)存大小蝎抽,再經(jīng)由16字節(jié)對齊(老版本是8字節(jié)對齊),計算出需要申請的內(nèi)存空間大小劫乱,所以對象的內(nèi)存大小只會是16的整數(shù)倍织中,并且最少申請16字節(jié)大小的內(nèi)存。
對內(nèi)存大小作16字節(jié)對齊或8字節(jié)對齊衷戈,主要是為了保證cpu讀取內(nèi)存數(shù)據(jù)的安全狭吼,以及提高讀取效率。
若數(shù)據(jù)類型為結(jié)構(gòu)體(
struct
)殖妇,在計算內(nèi)存大小時需要遵循結(jié)構(gòu)體內(nèi)存對齊原則刁笙,詳細介紹可參考下方推薦閱讀里的『數(shù)據(jù)結(jié)構(gòu) -- 結(jié)構(gòu)體Struct』一文。
推薦閱讀
1. iOS原理 OC對象的實例化
2. iOS原理 alloc核心步驟2:calloc詳解
3. iOS原理 alloc核心步驟3:initInstanceIsa詳解
4. 數(shù)據(jù)結(jié)構(gòu) -- 結(jié)構(gòu)體Struct