原文鏈接OC內(nèi)存大小的相關(guān)計(jì)算
更新于2020-07-13
在面試的過程中挺物,我們較大概率地會(huì)被問一個(gè)類所占的內(nèi)存大小。本篇博客從下面一段測試代碼開始分析整個(gè)內(nèi)存大小的計(jì)算過程飘弧。
測試代碼如下:
struct A {
} TestA;
struct AA {
char a;
} TestAA;
struct AAA {
char a;
int b;
} TestAAA;
@interface Person: NSObject {
int _a;
}
@end
@implementation Person
@end
@interface Student1: Person {
int _b;
}
@end
@implementation Student1
@end
@interface Student2: Person {
int _b;
int _c;
}
@end
@implementation Student2
@end
// 測試
+ (void)test {
NSLog(@"TestA sizeof: %lu",sizeof(TestA));
NSLog(@"TestAA sizeof: %lu",sizeof(TestAA));
NSLog(@"TestAAA sizeof: %lu",sizeof(TestAAA));
NSLog(@"--------------------------------------");
NSLog(@"NSObject class_getInstanceSize = %zd", class_getInstanceSize([NSObject class]));
NSLog(@"NSObject malloc_size = %zd", malloc_size((__bridge const void*)[NSObject new]));
NSLog(@"--------------------------------------");
NSLog(@"Person class_getInstanceSize = %zd", class_getInstanceSize([Person class]));
NSLog(@"Person malloc_size = %zd", malloc_size((__bridge const void*)[Person new]));
NSLog(@"--------------------------------------");
NSLog(@"Student1 class_getInstanceSize = %zd", class_getInstanceSize([Student1 class]));
NSLog(@"Student1 malloc_size = %zd", malloc_size((__bridge const void*)[Student1 new]));
NSLog(@"Student1 sizeof = %zd", sizeof([Student1 class]));
NSLog(@"--------------------------------------");
NSLog(@"Student2 class_getInstanceSize = %zd", class_getInstanceSize([Student2 class]));
NSLog(@"Student2 malloc_size = %zd", malloc_size((__bridge const void*)[Student2 new]));
}
執(zhí)行結(jié)果(運(yùn)行在模擬器下)如下:
其中class_getInstanceSize
指的是成員變量占用的內(nèi)存大小识藤,malloc_size
指的是指針指向內(nèi)存空間的大小即實(shí)際分配的內(nèi)存大小。
關(guān)于內(nèi)存大小的計(jì)算主要依賴于運(yùn)行環(huán)境以及內(nèi)存對(duì)齊次伶。上面的都是運(yùn)行在arm64環(huán)境下痴昧,因此內(nèi)存對(duì)齊決定了它們的值為什么不同。
內(nèi)存對(duì)齊
內(nèi)存對(duì)齊說白了就是為了提高CPU尋址操作性能的一種規(guī)則冠王。我們可以通過#pragma pack(n)
赶撰,n=1、2柱彻、4豪娜、8、16 來改變這一系數(shù)绒疗,其中的n就是要指定的“對(duì)齊系數(shù)”侵歇。內(nèi)存對(duì)齊的規(guī)則如下:
- 數(shù)據(jù)成員對(duì)齊規(guī)則:結(jié)構(gòu)體或聯(lián)合體的第一個(gè)數(shù)據(jù)成員放在偏移為0的位置,以后每個(gè)數(shù)據(jù)成員的位置為min(對(duì)齊系數(shù)吓蘑,自身長度)的整數(shù)倍,下個(gè)位置不為本數(shù)據(jù)成員的整數(shù)倍位置的自動(dòng)補(bǔ)齊。
- 數(shù)據(jù)成員為結(jié)構(gòu)體:該數(shù)據(jù)成員的內(nèi)最大長度的整數(shù)倍的位置開始存儲(chǔ)磨镶。
- 整體對(duì)齊規(guī)則:數(shù)據(jù)成員按照1溃蔫,2步驟對(duì)齊之后,其自身也要對(duì)齊琳猫,對(duì)齊原則是min(對(duì)齊系數(shù)伟叛,數(shù)據(jù)成員最大長度)的整數(shù)倍。
內(nèi)存對(duì)齊計(jì)算
在64位編譯器環(huán)境下
代碼示例1:
// 對(duì)齊系數(shù)為8
#pragma pack(8)
struct AA {
int a; // 4字節(jié)
char b; // 1字節(jié)
short c; // 2字節(jié)
char d; // 1字節(jié)
} Test1AA;
#pragma pack()
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"Test1AA: %lu",sizeof(Test1AA));
}
}
執(zhí)行結(jié)果:
Test1AA: 12
計(jì)算過程如下:
代碼示例2:
#pragma pack(8)
struct AA {
char a[2];
short b;
struct BB {
int a;
double b;
float c;
} Test2BB;
} Test2AA;
#pragma pack()
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"Test2AA: %lu",sizeof(Test2AA));
}
}
執(zhí)行結(jié)果:
Test2AA: 32
計(jì)算過程如下:
OC類的內(nèi)存分析
相關(guān)函數(shù)
class_getInstanceSize
上面講到class_getInstanceSize
表示成員變量所占的內(nèi)存大小脐嫂,那么對(duì)于Person類創(chuàng)建的實(shí)例來說统刮,它的成員變量大小應(yīng)該為12,為什么結(jié)果卻是16账千。
class_getInstanceSize
的內(nèi)存也有它自己的內(nèi)存對(duì)齊侥蒙,通過objc源碼中的class_getInstanceSize
的底層實(shí)現(xiàn)可以知道,class_getInstanceSize
的實(shí)現(xiàn)依賴于底層函數(shù)word_align
匀奏,該函數(shù)返回的結(jié)果是8的倍數(shù)鞭衩,另外從alloc
函數(shù)開始進(jìn)行分析,到instanceSize
函數(shù)中可以知道所有對(duì)象的內(nèi)存大小至少是16個(gè)字節(jié)娃善,所以對(duì)象申請(qǐng)的內(nèi)存空間是以8字節(jié)進(jìn)行內(nèi)存對(duì)齊且至少是16個(gè)字節(jié)论衍。
word_align
實(shí)現(xiàn)如下:
static inline uint32_t word_align(uint32_t x) {
// WORD_MASK在64下的定義為7UL,就是7聚磺,所以相當(dāng)于(x + 7) & ~7
// 0000 0111 -> 7
// x:12坯台,12就是Person類中成員變量的大小
// 12+7 = 19
// 0001 0011 -> 19
// &
// 1111 1000 -> ~7 ~運(yùn)算,二進(jìn)制中瘫寝,0變1蜒蕾,1變0.
// 0001 0000 -> 16
return (x + WORD_MASK) & ~WORD_MASK;
}
malloc_size
通過malloc源碼中的segregated_size_to_fit
函數(shù)可以知道系統(tǒng)開辟內(nèi)存空間是以16字節(jié)進(jìn)行內(nèi)存對(duì)齊。
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) {
// NANO_REGIME_QUANTA_SIZE: (1 << SHIFT_NANO_QUANTUM) 即 16
size = NANO_REGIME_QUANTA_SIZE;
}
// size: 8
// 0000 1000 -> 8
// size + NANO_REGIME_QUANTA_SIZE - 1 = 8 + 15 = 23
// 0001 0111 -> 23
// >> 4
// 0000 0001
// << 4
// 0001 0000 -> 16
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
slot_bytes = k << SHIFT_NANO_QUANTUM;
*pKey = k - 1;
return slot_bytes;
}
Person矢沿、Student的內(nèi)存分析
回到一開始的測試代碼滥搭,根據(jù)上面內(nèi)存對(duì)齊的3個(gè)規(guī)則,Person捣鲸、Student1瑟匆、Student2的實(shí)際分配內(nèi)存(malloc_size)計(jì)算過程如下:
使用View Memory查看內(nèi)存
從上面這張圖中我們可以知道,兩個(gè)16進(jìn)制代表1個(gè)字節(jié)栽惶,一行有32個(gè)字節(jié)愁溜。01 00 00 00
、03 00 00 00
外厂、05 00 00 00
(小端)對(duì)應(yīng)的就是_a
冕象、_b
、_c
汁蝶,另外前8個(gè)字節(jié)就是isa渐扮。整體內(nèi)存布局也與我們手動(dòng)計(jì)算實(shí)際內(nèi)存分配的結(jié)果一致论悴。
sizeof
sizeof
用來返回類型的大小,其內(nèi)部也是進(jìn)行了內(nèi)存對(duì)齊的墓律。將測試代碼中的結(jié)構(gòu)體A
膀估、AA
、AAA
通過內(nèi)存對(duì)齊規(guī)則進(jìn)行分析耻讽,確實(shí)得到0
察纯,1
,8
针肥。
這里我們使用結(jié)構(gòu)體AA
進(jìn)行舉例:
- 根據(jù)規(guī)則1饼记,數(shù)據(jù)成員占1個(gè)字節(jié),位于0號(hào)地址慰枕;
- 無結(jié)構(gòu)體成員變量具则,跳過規(guī)則2;
- 根據(jù)規(guī)則3捺僻,
min(1, n) = 1
乡洼,取1的整數(shù)倍,即結(jié)構(gòu)體分配的內(nèi)存大小為1個(gè)字節(jié)匕坯。
關(guān)于使用sizeof
去獲取OC類內(nèi)存大小的時(shí)候束昵,我發(fā)現(xiàn)一個(gè)比較有意思的東西。
測試代碼如下:
struct N_NSObject_IMPL {
Class isa;
};
struct N_Person_IMPL {
struct N_NSObject_IMPL NSObject_IVARS;
int _a;
};
struct N_Student_IMPL {
struct N_Person_IMPL Person_IVARS;
int _b;
char _c;
};
@interface Person : NSObject {
int _a;
}
@end
@implementation Person
@end
@interface Student : Person {
int _b;
char _c;
}
@end
@implementation Student
@end
// 測試
+ (void)test {
NSObject *o = [NSObject new];
struct N_NSObject_IMPL *so = (__bridge struct N_NSObject_IMPL *)o;
NSLog(@"[NSObject new] sizeof = %lu", sizeof(o));
NSLog(@"[NSObject class] sizeof = %lu", sizeof([NSObject class]));
NSLog(@"struct N_NSObject_IMPL sizeof = %lu", sizeof(struct N_NSObject_IMPL));
NSLog(@"*so sizeof = %lu", sizeof(so));
NSLog(@"--------------------------------------");
Person *p = [Person new];
struct N_Person_IMPL *sp = (__bridge struct N_Person_IMPL *)p;
NSLog(@"[Person new] sizeof = %lu", sizeof(p));
NSLog(@"[Person class] sizeof = %lu", sizeof([Person class]));
NSLog(@"struct N_Person_IMPL sizeof = %lu", sizeof(struct N_Person_IMPL));
NSLog(@"*sp sizeof = %lu", sizeof(sp));
NSLog(@"--------------------------------------");
Student *s = [Student new];
struct N_Student_IMPL *ss = (__bridge struct N_Student_IMPL *)s;
NSLog(@"[Student new] sizeof = %lu", sizeof(s));
NSLog(@"[Student class] sizeof = %lu", sizeof([Student class]));
NSLog(@"N_Student_IMPL sizeof = %lu", sizeof(struct N_Student_IMPL));
NSLog(@"Student malloc_size = %zd", malloc_size((__bridge const void*)s));
NSLog(@"*ss sizeof = %lu", sizeof(ss));
}
執(zhí)行結(jié)果:
N_NSObject_IMPL
葛峻、N_Person_IMPL
锹雏、N_Student_IMPL
是將OC轉(zhuǎn)成C++代碼時(shí)對(duì)應(yīng)的結(jié)構(gòu),使用sizeof
獲取這些結(jié)構(gòu)體大小的時(shí)候术奖,值與class_getInstanceSize
的值是一樣的礁遵。
使用sizeof
獲取指針、實(shí)例采记、類其結(jié)果都是8佣耐,這又是為什么呢?個(gè)人認(rèn)為傳實(shí)例和類的時(shí)候可以看做傳的其實(shí)就是對(duì)應(yīng)的指針唧龄。指針的值是指針本身存儲(chǔ)的數(shù)值兼砖,這個(gè)值將被編譯器當(dāng)作一個(gè)地址,地址基本就是整形既棺,因此無論用什么類讽挟、對(duì)象作為sizeof
的參數(shù)(這里就將sizeof
看成是一個(gè)函數(shù)),其結(jié)果都一樣的丸冕。
最后再總結(jié)下class_getInstanceSize
耽梅、malloc_size
、sizeof
的區(qū)別:
-
class_getInstanceSize
表示成員變量的所占的內(nèi)存大小 -
malloc_size
表示實(shí)際分配的內(nèi)存大小 -
sizeof
表示變量或者類型的大小胖烛,傳入結(jié)構(gòu)體眼姐,返回的則是結(jié)構(gòu)的大小诅迷,傳入指針(這里的指針表示C指針,OC的引用)即傳入值妥凳,則返回傳入值的類型大小