該文章是對前一篇 OC底層原理01-alloc流程探索 中cls->instanceSize
(計算內存大小)分支的一個拓展和深入
一带饱、查看內存地址
1.1 下斷點
1.2 用x
打印地址元媚,既可以x
該對象疼蛾,也可以x
該對象的指針地址
1.3 上面打印的一串16進制就是內存地址了,前面八位就是存的isa愉舔,但是因為iOS是小端儲存方式钢猛,所以應該倒著讀,那isa的地址實際為:
1.4 為了方便好看我們優(yōu)化一下打印命令指令:x/4gx
轩缤,按照每8位的格式打印4組
1.5 那我們來po
一下看看他里面到底存的什么東西
1.6 原來我們賦值的屬性存到這里的命迈,證明我們確實找到了他的存儲位置,那么為什么第一行第二組全是0呢火的?
1.7 原來是因為還有age
,height
,char1
,char2
沒賦值躺翻,為了驗證,我們將他們都賦值打印看看情況
1.8 我們發(fā)現(xiàn)當所有屬性都賦值過后卫玖,沒有出現(xiàn)0x000000000000的情況了公你,證明我們上面的猜想正確。那我們的age
,char1
,char2
假瞬,去哪了呢陕靠?是在0x0000001200004241
中嗎?但是他又是一串亂碼數(shù)字脱茉,接下來就是見證奇跡的時刻
1.9 為什么A變成了65剪芥,B變成了66呢?聰明的你肯定猜到了琴许,因為字符會以ASCII碼的形式存在內存税肪,那么問題又來了:
對象是以16字節(jié)對齊的方式開辟空間,它真正需要的內存是8字節(jié)對齊榜田,那為什么age
,char1
,char2
被存到了一組8字節(jié)的內存里面了呢益兄?
結論:系統(tǒng)進行了內存優(yōu)化,重排箭券,類的本質是結構體净捅,引入我們下一個探討話題
二、結構體中的內存對齊
2.1 C/OC 中各種數(shù)據(jù)類型的大小
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 | NSUInteger(64位) | 4 | 8 |
long long | int64_t | 8 | 8 |
float | CGFloat(32位) | 4 | 4 |
double | CGFloat(64位) | 8 | 8 |
2.2 探究結構體的內存大小
2.2.1 創(chuàng)建兩個結構體国章,并打印它的內存大小
struct GomuStruct1 {
double a;
char b;
int c;
short d;
} struct1;
struct GomuStruct2 {
double a;
int c;
char b;
short d;
} struct2;
NSLog(@"%lu - %lu",sizeof(struct1),sizeof(struct2));
//: log: 24 - 16
- 為什么只是交換了c,b的位置豆村,結構體的大小就變了液兽,24顯然也不是16的倍數(shù),說明結構體字節(jié)對齊和對象的字節(jié)對齊規(guī)則不相同
2.2.2 結構體的內存對齊規(guī)則一:
結構體/聯(lián)合體中你画,第一個數(shù)據(jù)成員以offset為0的地方開始抵碟,長度為成員大小桃漾,結束位置如果是第二個成員大小的整數(shù)倍,那么第二個數(shù)據(jù)成員就以此位置開始拟逮,長度為成員大小撬统,若非第二個成員大小的整數(shù)倍,往后推移敦迄,直到該數(shù)字是第二個成員大小的整數(shù)倍恋追,作為第二個成員的開始位置,第三個以此內推罚屋。
2.2.3 結構體的內存對齊規(guī)則二:
結構體的最終大小苦囱,也就是sizeof的結果,必須是其內部最大成員大小的整數(shù)倍脾猛,不足就要補齊撕彤。
//: 通過上面規(guī)則一計算結構體內存大小,與打印一致
struct GomuStruct1 {
double a; // 8 (0~7)
char b; // 1 (8)[1,8],8是1的整數(shù)倍
int c; // 4 (12~15)[4,9],9不是4的整數(shù)倍猛拴,往后推移到 10羹铅,11,12
short d; // 2 (16~17)[2,16],16是2的整數(shù)倍
} struct1;
// 內部需要的大小為:18(0~17)
// 最大屬性:a 8字節(jié)
// 結構體整數(shù)倍:8的整數(shù)倍:24(18不足補位)[通過上面規(guī)則二得出]
//: 通過上面規(guī)則一計算結構體內存大小愉昆,與打印一致
struct GomuStruct2 {
double a; // 8 (0~7)
int c; // 4 (8~11),[4,8],8是4的整數(shù)倍
char b; // 1 (11~12),[1,12],12是1的整數(shù)倍
short d; // 2 (14~15),[2,13],13不是2的整數(shù)倍职员,往后推移到 14
} struct2;
// 內部需要的大小為:16(0~15)
// 最大屬性:a 8字節(jié)
// 結構體整數(shù)倍:8的整數(shù)倍:16(16是8的整數(shù)倍)[通過上面規(guī)則二得出]
注意:結構體指針的內存大小8字節(jié),而結構體內部的內存大小是根據(jù)內部成員來進行開辟的跛溉。
2.2.4 結構體的內存對齊規(guī)則三:(針對嵌套結構體)
結構體中嵌套結構體焊切,結構體成員要從其內部最大元素大小的整數(shù)倍地址開始存儲。
struct GomuStruct2 {
double a; // 8 (0~7)
int c; // 4 (8~11),[4,8],8是4的整數(shù)倍
char b; // 1 (12),[1,12],12是1的整數(shù)倍
short d; // 2 (14~15),[2,13],13不是2的整數(shù)倍芳室,往后推移到 14
} struct2;
struct GomuStruct3 {
double a; // 8 (0~7)
int c; // 4 (8~11),[4,8],8是4的整數(shù)倍
char b; // 1 (12),[1,12],12是1的整數(shù)倍
short d; // 2 (14~15),[2,13],13不是2的整數(shù)倍专肪,往后推移到 14
struct GomuStruct2 struct2; // 8 (16,31),[8,16],16是8的整數(shù)倍,往后推移到struct2的大小16的位置:31
} struct3; // (0~31) 32渤愁,32是最大8的整數(shù)倍牵祟,所以為32
2.2.5 結構體的內存對齊規(guī)則四:(針對嵌套結構體)
如果把結構體成員放在第一位,那它依然遵守結構體內存規(guī)則抖格,從offset為0的地方開始
struct GomuStruct2 {
double a; // 8 (0~7)
int c; // 4 (8~11),[4,8],8是4的整數(shù)倍
char b; // 1 (12),[1,12],12是1的整數(shù)倍
short d; // 2 (14~15),[2,13],13不是2的整數(shù)倍,往后推移到 14
} struct2;
struct GomuStruct3 {
struct GomuStruct2 struct2; // 8 (0~15)
char b; // 1 (16)
} struct3; // (0~16) 17咕晋,17不是最大8的整數(shù)倍雹拄,補齊所以為24
2.2.6 結構體的內存對齊規(guī)則五:(針對嵌套結構體)
結構體中嵌套結構體,最后結構體的大小不是結構體成員大小的整數(shù)倍掌呜,而是結構體成員中的成員(即嵌套結構體中的最大成員)的大小/結構體中的最大成員(結構體中除去嵌套結構體的最大成員)的大小的整數(shù)倍滓玖。如上面,24是struct3 的大小质蕉,16是struct2 的大小势篡,24不是16的整數(shù)倍翩肌,他是struct2中 double a 的大小8的整數(shù)倍
2.2.7 根據(jù)結構體內存對齊規(guī)則畫出struct1的內存圖如下:
為什么要這么分配?
以空間換時間禁悠,方便讀取念祭。
2.2.8 結構體內存讀取步驟:
- 以最大成員大小 這里的struct1 是 8 字節(jié)為尺度讀取,分成三段碍侦,
0~7
粱坤,8~15
,16~23
瓷产。 - 然后再在每個尺度中按照該尺度的最大成員去讀取站玄,如第一段(
0~7
),最大是8濒旦,則直接讀出a(0~7
)株旷,第二段(8~15
),最大是4尔邓,讀出8~11
晾剖,12~15
,讀出c(12~15
)铃拇。 - 再在
8~11
中取最大1钞瀑,則讀出b:(8)。 - 以此類推慷荔,也可以讀出d
三雕什、類的內存開辟規(guī)則
3.1 分別打印sizeof
,class_getInstanceSize
,malloc_size
GomuPerson *person = [GomuPerson alloc];
person.name = @"Gomu";
person.nickName = @"iOS";
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([GomuPerson class]),malloc_size((__bridge const void *)(person)));
//: <GomuPerson: 0x10050ca10> - 8 - 40 - 48
-
sizeof
打印的是person這個指針所占的內存大小:8 -
class_getInstanceSize
打印的是GomuPerson對象所需要的真正內存:40 -
malloc_size
打印的則是系統(tǒng)給person開辟的內存:48 -
成員變量
显晶、方法
不占用對象內存贷岸。
3.2 class_getInstanceSize
中的內存開辟規(guī)則
3.2.1 進入objc4-781.2源碼,搜索class_getInstanceSize
3.2.2 進入class_getInstanceSize
方法
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
3.2.3 進入alignedInstanceSize
方法
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
3.2.3 進入word_align
方法
# define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
// 舉例 x 為 2
// x + WORD_MASK 2 + 7 = 9
// 轉換成2進制 0000 1001
// WORD_MASK = 7 轉換成二級制 0000 0111
// ~WORD_MASK 1111 1000
// (x + WORD_MASK) & ~WORD_MASK
// 0000 1001
// 1111 1000
// 0000 1000 //因為7取反后磷雇,后三位肯定是0偿警,與任何值&運算,后三位都為0
// 所以該算法的結果必然是8的倍數(shù)
-
WORD_MASK
這個宏是7唯笙。 -
(x + WORD_MASK) & ~WORD_MASK
這是一個取8的倍數(shù)的算法螟蒸。 - 對象中真正的對齊方式是8字節(jié)對齊。
3.3 alloc
初始化三部曲中崩掘,第一步cls->instanceSize(extraBytes)
中的內存開辟規(guī)則
//: cls->instanceSize(extraBytes)七嫌,該方法最終會來到`align16 `方法
//: 該算法在前一篇中已經講解,這里就不過多贅述
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
-
cls->instanceSize(extraBytes)
方法是取16的倍數(shù)的一個算法苞慢。 -
alloc
初始化三部曲中诵原,第一步就是上述方法,計算對象的內存空間。 - 系統(tǒng)給對象分配內存的方式是16字節(jié)對齊绍赛。
3.4 alloc
初始化三部曲中蔓纠,第二步calloc
中的內存開辟規(guī)則
3.4.1 由符號斷點得出,calloc
在libsystem_malloc.dylib
庫中
3.4.2 由于當前源碼在libobjc.A.dylib
中吗蚌,所以我們無法進入源碼調試calloc
腿倚,為了研究calloc
,配置了一份libsystem_malloc.dylib
的可編譯源碼
3.4.3 開始調式褪测,創(chuàng)建一個calloc
對象
void *p = calloc(1, 32); //: 在這里打個斷點開始調試
3.4.4 進入calloc(size_t num_items, size_t size)
void *
calloc(size_t num_items, size_t size)
{
void *retval;
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
- 重點:
default_zone
猴誊,(下回分解)
3.4.5 進入malloc_zone_calloc(default_zone, num_items, size)
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
ptr = zone->calloc(zone, num_items, size);
if (malloc_logger) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
return ptr;
}
3.4.6 到zone->calloc(zone, num_items, size)
這里就沒了
- 當不能再進入的時候,在
zone->calloc(zone, num_items, size)
處加個斷點
- 執(zhí)行
po zone->calloc
侮措,拿到下個流程default_zone_calloc
3.4.7 進入default_zone_calloc
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();
return zone->calloc(zone, num_items, size);
}
- 又出現(xiàn)
zone->calloc(zone, num_items, size)
懈叹,同樣的方法我們拿到下一個流程nano_calloc
3.4.8 進入nano_calloc
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}
if (total_bytes <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
- 定位到關鍵代碼
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
3.4.9 進入_nano_malloc_check_clear
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
void *ptr;
size_t slot_key;
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
mag_index_t mag_index = nano_mag_index(nanozone);
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
if (ptr) {... 該部分代碼省略}else {
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}
if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}
- 別忘了我們的目的,探索calloc開辟內存的規(guī)則分扎,所以抓住
size
不放澄成,得到核心方法segregated_size_to_fit
3.4.10 進入核心方法segregated_size_to_fit
#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;
}
-
SHIFT_NANO_QUANTUM
的值是4 -
NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)
這個的值1往左移4位,即 0000 0001 -> 0001 0000 畏吓,所以他的值是16 - 第一步墨状,如果
size
為0,給size
賦值16 - 第二步菲饼,
size
+NANO_REGIME_QUANTA_SIZE - 1
肾砂,即 先 + 15 ,然后往右移 4 位 - 第三步宏悦,再往左移 4 位
//: 舉例:假如 size = 4
//: 4+15 = 21 , 二進制為: 0001 0101
//: 右移4位: 0000 0001
//: 做移4位: 0001 0000
//: 結果為16
//: 左移4位镐确,再右移4位,即對后4位置0饼煞,那得到的結果必然為16的倍數(shù)
- 這個算法和
(x + size_t(15)) & ~size_t(15)
這個作用一樣源葫。 - 探索calloc再次說明,開辟內存的時候砖瞧,系統(tǒng)是以16字節(jié)對齊進行開辟息堂。
四、拓展
4.1 sizeof
-
sizeof
是一個運算符
块促,并不是一個函數(shù)
荣堰。 - 用于計算
參數(shù)的類型大小
,而不是參數(shù)值
int a = 1;
double b = 2.0;
GomuPerson *person = [GomuPerson alloc];
NSLog(@"%lu - %lu - %lu",sizeof(a),sizeof(b),sizeof(person));
NSLog(@"%lu - %lu - %lu",sizeof(int),sizeof(double),sizeof(void *));
//: 打印都為 : 4 - 8 - 8
//: sizeof(a) 等價于 sizeof(int)
//: sizeof(b) 等價于 sizeof(double)
//: sizeof(person) 等價于 sizeof(void *) person 是一個結構體指針
4.2 LLDB 調試常用指令
(lldb) po person
<GomuPerson: 0x10108e740>
(lldb) p person
(GomuPerson *) $24 = 0x000000010108e740
(lldb) x person
0x10108e740: b5 23 00 00 01 80 1d 00 00 00 00 00 00 00 00 00 .#..............
0x10108e750: 10 10 00 00 01 00 00 00 30 10 00 00 01 00 00 00 ........0.......
(lldb) memory read person
0x10108e740: b5 23 00 00 01 80 1d 00 00 00 00 00 00 00 00 00 .#..............
0x10108e750: 10 10 00 00 01 00 00 00 30 10 00 00 01 00 00 00 ........0.......
(lldb) x/4gx person
0x10108e740: 0x001d8001000023b5 0x0000000000000000
0x10108e750: 0x0000000100001010 0x0000000100001030
(lldb) x/4gw person
0x10108e740: 0x000023b5 0x001d8001 0x00000000 0x00000000
(lldb) p/t 15
(int) $29 = 0b00000000000000000000000000001111
(lldb) p/x 15
(int) $30 = 0x0000000f
(lldb)
-
po
:打印對象 -
p
: 打印基本數(shù)據(jù)類型 -
x
和memory read
: 以16進制的方式竭翠,打印對象的內存情況 -
x/4gx
:以16進制的方式持隧,打印4段8字節(jié)的內存地址 -
x/4gw
:以16進制的方式,打印4段4字節(jié)的內存地址 -
p/t
: 轉換成2進制打印 -
p/x
: 轉換成16進制打印
4.3 double/float
在內存的存儲方式
-
double/float
會轉成16進制存逃片,直接po不出來
//: person.height = 180.1; 給height定義成double
(lldb) x/6gx person
0x10182b960: 0x001d8001000033c5 0x0000001200000000
0x10182b970: 0x0000000100002010 0x0000000100002030
0x10182b980: 0x4066833333333333 0x0000000000000000
(lldb) po 0x4066833333333333
4640540721977439027
(lldb) p/x 180.1
(double) $13 = 0x4066833333333333