iOS探索 記憶體對齊&malloc原始碼

我是好寶寶發表於2020-01-02

歡迎閱讀iOS探索系列(按序閱讀食用效果更加)

寫在前面

iOS探索 alloc流程一文中講了底層物件建立的流程,那麼本文將來探索下物件中的屬性在記憶體中的排列

一、探索目標

1.測試程式碼

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

@interface FXPerson : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) long height;
@property (nonatomic, assign) char c1;
@property (nonatomic, assign) char c2;
@end

@implementation FXPerson

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXPerson *p = [FXPerson alloc];
        p.name = @"Felix";
        p.age = 20;
        p.height = 180;
        p.c1 = 'a';
        p.c2 = 'b';
        
        NSLog(@"\nsizeof——%lu\nclass_getInstanceSize——%lu\nmalloc_size——%lu", sizeof([p class]), class_getInstanceSize([p class]), malloc_size((__bridge const void *)(p)));
    }
    return 0;
}
複製程式碼

**注:如果物件建立了沒去賦值屬性——它會是記憶體假地址

2.LLDB除錯命令等預備知識

x 物件表示以16進位制列印物件記憶體地址(x表示16進位制)

因為iOS是小端模式(資料的高位元組儲存在記憶體的高地址中,而資料的低位元組儲存在記憶體的低地址中——反過來存放資料)所以要倒著讀資料

(lldb) x p
0x10060eea0: c5 13 00 00 01 80 1d 00 61 62 00 00 00 00 00 00  ........ab......
0x10060eeb0: 14 00 00 00 00 00 00 00 50 10 00 00 01 00 00 00  ........P.......
複製程式碼

x/4gx 物件表示輸出4個16進位制的8位元組地址空間(x表示16進位制,4表示4個,g表示8位元組為單位,等同於x/4xg 物件

(lldb) x/4gx p
0x10060eea0: 0x001d8001000013c5 0x0000000000006261
0x10060eeb0: 0x0000000000000014 0x0000000100001050
複製程式碼

左邊是記憶體地址,右邊兩段是記憶體值

pop:p表示"expression"——列印物件指標;而po是"expression -O"——列印物件本身

(lldb) p p
(FXPerson *) $0 = 0x0000000101857750
(lldb) po p
<FXPerson: 0x101857750>
複製程式碼

④Xcode檢視記憶體地址 debug->Debug Workflow->view memory

iOS探索 記憶體對齊&malloc原始碼

有些操作可能用不到,讀者可以自行擴充

3.修改程式碼檢視記憶體

FXPerson類中先宣告name,再宣告age

(lldb) x/6gx p
0x10062c380: 0x001d8001000013c5 0x0000000000006261
0x10062c390: 0x0000000100001050 0x0000000000000014
0x10062c3a0: 0x00000000000000b4 0x0000000000000000
複製程式碼

FXPerson類中先宣告age,再宣告name

(lldb) x/6gx p
0x100538e00: 0x001d8001000013c5 0x0000000000006261
0x100538e10: 0x0000000000000014 0x0000000100001050
0x100538e20: 0x00000000000000b4 0x0000000000000000
複製程式碼

根據我們的計算機基礎和LLDB指令,可以發現

  • 第一段不知道是啥
  • 第二段中62、63分別是ab的ASCII編碼
  • 第三段中的14是20的十六進位制
  • 第四段中po出來是Felix
  • 第五段是180

4.檢視控制檯輸出

sizeof——8
class_getInstanceSize——40
malloc_size——48
複製程式碼

5.去掉宣告屬性檢視控制檯輸出

FXPerson類中不宣告任何屬性

sizeof——8
class_getInstanceSize——8
malloc_size——16
複製程式碼

6.提出問題

Q1:為什麼宣告屬性的前後會影響物件的記憶體排列呢?

Q2:sizeof、class_getInstanceSize、malloc_size分別是什麼?

Q3:不是說物件最少為16位元組,為什麼class_getInstanceSize還能輸出8位元組?

二、記憶體對齊

1.二進位制重排

二進位制重排——將最經常執行的程式碼或最需要關鍵執行的程式碼(如啟動階段的順序呼叫)聚合在一起,將無關緊要的程式碼放在較低的優先順序,形成一個更緊湊的__TEXT段

2.記憶體優化

如果按照物件預設宣告的屬性順序進行記憶體分配,在進行屬性的8位元組對齊時會浪費大量的記憶體空間,所以這裡系統會把物件的屬性重新排列,以此來最大化利用我們的記憶體空間——與二進位制重排有著異曲同工之妙

3.sizeof、class_getInstanceSize、malloc_size

sizeof:它是一個運算子,在編譯時就可以獲取型別所佔記憶體的大小

class_getInstanceSize:依賴於<objc/runtime.h>,返回建立一個例項物件所需記憶體大小

malloc_size:依賴於<malloc/malloc.h>,返回系統實際分配的記憶體大小


關於class_getInstanceSize還能輸出8位元組

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
複製程式碼

對於class_getInstanceSize為什麼會返回8位元組,這是一個易錯題!!!! 在objc原始碼中搜尋class_getInstanceSize,會發現它只做了位元組對齊——8位元組對齊,而alloc一文中講過的至少為16位元組程式碼不包含在class_getInstanceSize呼叫棧中——if (size < 16) size = 16;

4.記憶體對齊原則

物件的屬性要記憶體對齊,而物件本身也需要進行記憶體對齊

  • 資料成員對齊原則: 結構(struct)(或聯合(union))的資料成員,第 一個資料成員放在offset為0的地方,以後每個資料成員儲存的起始位置要 從該成員大小或者成員的子成員大小
  • 結構體作為成員:如果一個結構裡有某些結構體成員,則結構體成員要從 其內部最大元素大小的整數倍地址開始儲存
  • 收尾工作:結構體的總大小,也就是sizeof的結果,必須是其內部最大 成員的整數倍,不足的要補⻬

5.舉個例子

struct struct1 {
    char a;
    double b;
    int c;
    short d;
} str1;

struct struct2 {
    double b;
    char a;
    int c;
    short d;
} str2;

struct struct3 {
    double b;
    int c;
    char a;
    short d;
} str3;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%lu——%lu——%lu", sizeof(str1), sizeof(str2), sizeof(str3));
    }
    return 0;
}
複製程式碼

輸出結果為24——24——16

已知(64位)char為1位元組,double為8位元組,int為4位元組,short為2位元組

記憶體對齊原則其實可以簡單理解為min(m,n)——m為當前開始的位置,n為所佔位數。當m是n的整數倍時,條件滿足;否則m位空餘,m+1,繼續min演算法。

str1中的b,一開始為min(1,8),不滿足條件直至min(8,8),所以它在第8位坐下了,佔據8個格子

str2中的c,一開始為min(9,4),不滿足條件直至min(12,4),所以它在第12位坐下了,佔據4個格子

str3中的d,一開始為min(13,2),不滿足條件直至min(14,2),所以它在第14位坐下了,佔據2個格子

記憶體佈局示意圖

搜狐公眾號的一篇推送——記憶體佈局(講的很詳細,推薦閱讀)

三、malloc流程

關於記憶體開闢,還有一個歷史遺留性問題——alloc在底層申請記憶體空間時呼叫了obj = (id)calloc(1, size)。之前只有objc原始碼我們無從下手,現在我們可以通過libmalloc原始碼來一探究竟

1.calloc

libmalloc原始碼中新建target,按照objc原始碼中的方式呼叫

void *p = calloc(1, 40);
複製程式碼

2.malloc_zone_calloc

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;
}
複製程式碼

根據return retval可知retval是核心內容,所以去看看malloc_zone_calloc

3 default_zone_calloc

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;
}
複製程式碼

根據return ptr可知ptr是重點,但是ptr = zone->calloc(zone, num_items, size);跟進去會看到讓人一串摸不到頭腦的程式碼

void 	*(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
複製程式碼

3.1 方法一——分析zone

已知zone是malloc_zone_t型別的,在第二步中retval = malloc_zone_calloc(default_zone, num_items, size);中傳遞的第一個引數zone又是default_zone,跟蹤進去會發現它是一個靜態變數

static malloc_zone_t *default_zone = &virtual_default_zone.malloc_zone;
複製程式碼
static virtual_default_zone_t virtual_default_zone
__attribute__((section("__DATA,__v_zone")))
__attribute__((aligned(PAGE_MAX_SIZE))) = {
	NULL,
	NULL,
	default_zone_size,
	default_zone_malloc,
	default_zone_calloc,
	default_zone_valloc,
	default_zone_free,
	default_zone_realloc,
	default_zone_destroy,
	DEFAULT_MALLOC_ZONE_STRING,
	default_zone_batch_malloc,
	default_zone_batch_free,
	&default_zone_introspect,
	10,
	default_zone_memalign,
	default_zone_free_definite_size,
	default_zone_pressure_relief,
	default_zone_malloc_claimed_address,
};
複製程式碼

初步推測zone->allocdefault_zone_calloc

3.2 方法二——控制檯列印

有時候列印也是閱讀原始碼的一種方法——由列印可知實際呼叫default_zone_calloc

iOS探索 記憶體對齊&malloc原始碼

3.3 結論

只要思想不滑坡,方法總比困難多

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);
}
複製程式碼

4.nano_malloc

好不容易從malloc_zone_calloc找到了default_zone_calloc,然後又是熟悉的味道——zone->calloc(zone, num_items, size)

繼續列印試試

iOS探索 記憶體對齊&malloc原始碼

5._nano_malloc_check_clear

nano_malloc(nanozone_t *nanozone, size_t size)
{
	if (size <= NANO_MAX_SIZE) {
		void *p = _nano_malloc_check_clear(nanozone, size, 0);
		if (p) {
			return p;
		} else {
			/* FALLTHROUGH to helper zone */
		}
	}

	malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
	return zone->malloc(zone, size);
}
複製程式碼

shift+command+O來到nano_malloc

分析:這個方法中有兩個return和一句註釋/* FALLTHROUGH to helper zone */——進入輔助區域,即正常情況下走if判斷(如果要開闢的空間小於 NANO_MAX_SIZE 則進行nanozone_t的malloc)NANO_MAX_SIZE=256

6.segregated_size_to_fit

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;
}
複製程式碼

分析:此時此刻看到這麼長的一段程式碼也不用慌張,if-else只走其一。再仔細想想,我們是帶著目的來看原始碼的——malloc_size中的48是怎麼來的。這裡有多個size_t類,斷點除錯看了下的size是我們傳進來的40,而slot_bytes剛好是我們的目標48,那我們就來看下40->48是怎麼來的

iOS探索 記憶體對齊&malloc原始碼

7. 16位元組對齊

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
	// size = 40
	size_t k, slot_bytes;

	if (0 == size) {
		size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
	}
	// 40 + 16-1 >> 4 << 4
	// 40 - 16*3 = 48

	//
	// 16
	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;
}
複製程式碼

分析:size 是 40,在經過 (40 + 16 - 1) >> 4 << 4 操作後,結果為48,也就是16的整數倍——即16位元組對齊

8.malloc總結

物件的屬性是8位元組對齊

物件是16位元組對齊

  • 因為記憶體是連續的,通過 16 位元組對齊規避風險和容錯,防止訪問溢位
  • 同時,也提高了定址訪問效率,也就是空間換時間

9.malloc部分流程圖

iOS探索 記憶體對齊&malloc原始碼
更多malloc內容請看Cooci的malloc分析

寫在後面

關於寫文章,我喜歡先系統性的學一遍,將整體大綱寫下來,但是有可能在後續學習中會有新的感悟,會不定時將文章更新,但文章主要知識點都是正確無誤的

相關文章