一脓魏、instanceSize方法的底層實(shí)現(xiàn)
首先需要知道罢猪,對象的內(nèi)存空間里存放的是對象的屬性师抄,所以計(jì)算內(nèi)存大小即為計(jì)算所有屬性所占的內(nèi)存大小谐区,先看下objc源碼的底層實(shí)現(xiàn):
size_t instanceSize(size_t extraBytes) const {
//方式一:編譯器快速計(jì)算內(nèi)存大泻伞(16字節(jié)對齊)
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
//方式二:計(jì)算類中所有屬性和方法的內(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
方法的底層實(shí)現(xiàn)可能是不一樣的,本文參照的是最新的objc-781
源碼坤邪,里面會有這兩種計(jì)算方式熙含,但是實(shí)際運(yùn)行,其實(shí)是以方式一來計(jì)算的罩扇,16字節(jié)內(nèi)存對齊婆芦。而在老版本的objc源碼中怕磨,比如objc-750
源碼喂饥,里面只有方式二的計(jì)算方式消约,采取的是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;
}
雖然這個方法的底層實(shí)現(xiàn)不會頻繁更新员帮,但大家以后在分析時或粮,最好是參照最新的objc源碼進(jìn)行分析。話不多說捞高,接下來分析這兩種方式具體是怎么計(jì)算的氯材。
1.1 十六字節(jié)對齊 - fastInstanceSize
從源碼中可知,是在fastInstanceSize
方法里完成內(nèi)存計(jì)算的:
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);
}
}
編譯器快速計(jì)算出需要的size后硝岗,最后會執(zhí)行align16
方法氢哮,即『16字節(jié)對齊』。
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
系統(tǒng)在計(jì)算出內(nèi)存size后型檀,會先加15冗尤,再與個非15,這樣就實(shí)現(xiàn)了16字節(jié)對齊胀溺。接下來通過兩組二進(jìn)制運(yùn)算來直觀地看看字節(jié)對齊后的結(jié)果:
//已知: 15的二進(jìn)制為: 0000 1111
// ~15的二進(jìn)制為:1111 0000
//假設(shè)計(jì)算出當(dāng)前類需要的內(nèi)存大小為12字節(jié)裂七,則16字節(jié)對齊的計(jì)算過程如下:
0000 1100 // 12字節(jié)
0000 1111 // +15
0001 1011 // = 27
1111 0000 // ~15
0001 0000 // 27 & (~15) = 16
//假設(shè)計(jì)算出當(dāng)前類需要的內(nèi)存大小為24字節(jié),則16字節(jié)對齊的計(jì)算過程如下:
0001 1000 // 24字節(jié)
0000 1111 // +15
0010 0111 // = 39
1111 0000 // ~15
0010 0000 // 39 & (~15) = 32
從上面兩組二進(jìn)制運(yùn)算得知仓坞,12字節(jié)背零、24字節(jié)的內(nèi)存經(jīng)由16字節(jié)對齊后,實(shí)際申請內(nèi)存大小分別為16字節(jié)无埃、32字節(jié)徙瓶。因此,不管對象屬性實(shí)際需要多大的內(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;
在實(shí)例化對象時,經(jīng)由斷點(diǎn)調(diào)試可知蒲稳,extraBytes為0氮趋,所以內(nèi)存大小依賴于alignedInstanceSize
方法計(jì)算:
uint32_t alignedInstanceSize() {
//step1:通過unalignedInstanceSize方法獲取到需要的內(nèi)存大小
//step2:通過word_align方法對內(nèi)存大小進(jìn)行8字節(jié)運(yùn)算
return word_align(unalignedInstanceSize());
}
可以看到,class_getInstanceSize方法最終實(shí)現(xiàn)就兩步江耀,先獲取到對象屬性所占的內(nèi)存大小剩胁,再將內(nèi)存大小進(jìn)行8字節(jié)對齊運(yùn)算。
- unalignedInstanceSize
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
這一步獲取到的是對象的屬性所占的內(nèi)存大小祥国,ro是個很重要的知識點(diǎn)昵观,會在后續(xù)介紹類和對象的結(jié)構(gòu)時作詳細(xì)講解晾腔,本篇文章就不作過多拓展了。
- word_align
#define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
WORD_MASK的值為7啊犬,這里其實(shí)是對內(nèi)存大小作8字節(jié)對齊運(yùn)算灼擂。對齊計(jì)算和上文的16進(jìn)制對齊一樣,大家可以自己進(jìn)行二進(jìn)制演算觉至,這里就不再推演了剔应。在完成8字節(jié)對齊運(yùn)算后,還會再判斷size的大小语御,如果小于16峻贮,就申請16字節(jié)的內(nèi)存。 也就是說应闯,不管對象的屬性實(shí)際需要多大的內(nèi)存纤控,在老版本中,系統(tǒng)都會以8字節(jié)的倍數(shù)來申請內(nèi)存空間碉纺,并且最少16字節(jié)船万。
二、對內(nèi)存進(jìn)行字節(jié)對齊的原因
理解了內(nèi)存對齊邏輯后惜辑,可能會有疑問唬涧,為什么要對內(nèi)存進(jìn)行字節(jié)對齊呢?為什么不按對象屬性實(shí)際占用的內(nèi)存大小來分配內(nèi)存呢盛撑?經(jīng)由字節(jié)對齊后碎节,分配的空間要比實(shí)際需要的多,這樣會不會造成內(nèi)存浪費(fèi)抵卫?
在解答這些疑問前狮荔,我們需要先清楚,對象里可能會有多種數(shù)據(jù)類型的屬性介粘,這些屬性占用內(nèi)存大小是不一樣的殖氏,如果直接按照實(shí)際需要內(nèi)存進(jìn)行分配,則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é)為一段進(jìn)行讀取,既能保證每個數(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)如果以屬性實(shí)際占用內(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)如果以屬性實(shí)際占用內(nèi)存大小來分配,CPU讀取內(nèi)存數(shù)據(jù)時既不安全蕾盯,效率也低幕屹。
所以系統(tǒng)會對內(nèi)存進(jìn)行字節(jié)對齊,內(nèi)存分配情況大致如下(注:不是一定按照這個順序的):
若進(jìn)行字節(jié)對齊级遭,在分配內(nèi)存時望拖,會進(jìn)行屬性重排,將age装畅、c1靠娱、c2(總共6字節(jié))這三個屬性存在一個8字節(jié)段中,在讀取數(shù)據(jù)時掠兄,CPU也是按照8字節(jié)為一段來讀取像云,這樣既能保證讀取數(shù)據(jù)的完整性锌雀,又能提高讀取效率。雖說會多開辟幾字節(jié)的內(nèi)存空間迅诬,但是保證了讀取安全腋逆,提高了讀取效率,完全是值得的侈贷。而且鑒于現(xiàn)在設(shè)備內(nèi)存越來越大惩歉,這種『以空間換時間』來提高效率的方式,會越來越常見俏蛮。
三撑蚌、計(jì)算對象申請的內(nèi)存空間大小
內(nèi)存空間里保存的是對象的屬性,所以計(jì)算內(nèi)存空間大小即是統(tǒng)計(jì)對象所有屬性所占的內(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)存計(jì)算中,都需要加上其占用的8字節(jié)內(nèi)存携狭。
按照前面的思路继蜡,計(jì)算對象實(shí)例化時申請的內(nèi)存空間大小,即先統(tǒng)計(jì)所有屬性所占的內(nèi)存大小暑中,然后再進(jìn)行16字節(jié)對齊就可得到壹瘟。那怎么印證這個計(jì)算規(guī)則呢?最直觀的方法就是獲取并打印對象的內(nèi)存size鳄逾,之前有看過其他文章稻轨,很多都是通過class_getInstanceSize
方法來獲取的,先來看下這個方法的實(shí)現(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é)對齊就不適用了。在新版本里可以改寫這個方法的對齊運(yùn)算汽摹,然后再使用李丰,比如這樣:
//將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é)對齊計(jì)算
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);
- 最后實(shí)例化Person對象嗜憔,打印結(jié)果秃励。為了對比更加直觀,也打印出通過
class_getInstanceSize
方法獲取到的內(nèi)存大小吉捶。
準(zhǔn)備工作完成夺鲜,那接下來分析在各種情況下創(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)存大小進(jìn)行了判斷浪感,如果小于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ī)則苛谷,當(dāng)結(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)存大小的計(jì)算規(guī)則:先統(tǒng)計(jì)所有屬性所占的內(nèi)存大小,然后再進(jìn)行16字節(jié)對齊孝偎。簡單數(shù)據(jù)類型的屬性所占內(nèi)存比較容易統(tǒng)計(jì)访敌,但對于比較復(fù)雜的數(shù)據(jù)結(jié)構(gòu),比如說結(jié)構(gòu)體(struct
)邪媳,它的內(nèi)存計(jì)算就需要遵循『內(nèi)存對齊規(guī)則』捐顷。
四、總結(jié)
對象的內(nèi)存大小依賴其包含的屬性雨效,所以在實(shí)例化對象時迅涮,系統(tǒng)會先統(tǒng)計(jì)對象所有屬性所占的內(nèi)存大小,再經(jīng)由16字節(jié)對齊(老版本是8字節(jié)對齊)徽龟,計(jì)算出需要申請的內(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
)朱盐,在計(jì)算內(nèi)存大小時需要遵循結(jié)構(gòu)體內(nèi)存對齊原則群嗤,詳細(xì)介紹可參考下方推薦閱讀里的『數(shù)據(jù)結(jié)構(gòu) -- 結(jié)構(gòu)體Struct』一文。
五兵琳、幫助
在打印內(nèi)存大小時狂秘,可能會需要下面這三個方法:
-
sizeof
:是一個運(yùn)算符,用來計(jì)算傳進(jìn)來的數(shù)據(jù)類型占用多大的內(nèi)存躯肌,在編譯時即可完成運(yùn)算者春。 -
class_getInstanceSize
:獲取對象實(shí)例化時需要申請的內(nèi)存大小,采取的是8字節(jié)對齊方式清女,調(diào)用時需要先引用#import <objc/runtime.h>
钱烟。 -
malloc_size
:獲取對象實(shí)例化時系統(tǒng)實(shí)際開辟的內(nèi)存大小,采取的是16字節(jié)對齊方式嫡丙,調(diào)用時需要先引用#import <malloc/malloc.h>
拴袭。