在iOS探索 alloc流程一文中講了底層對象建立的流程,那麼本文未來探索下對象中的屬性在內存中的排列c++
#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;
}
複製代碼
注:若是對象建立了沒去賦值屬性——它會是內存假地址——它是一個野指針objective-c
①x 對象
表示以16進制打印對象內存地址(x表示16進制)算法
由於iOS是小端模式
(數據的高字節保存在內存的高地址中,而數據的低字節保存在內存的低地址中——反過來存放數據)因此要倒着讀數據express
(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 對象
)bash
(lldb) x/4gx p
0x10060eea0: 0x001d8001000013c5 0x0000000000006261
0x10060eeb0: 0x0000000000000014 0x0000000100001050
複製代碼
左邊是內存地址,右邊兩段是內存值 佈局
③po
與p
:p表示"expression"——打印對象指針;而po是"expression -O"——打印對象自己post
(lldb) p p
(FXPerson *) $0 = 0x0000000101857750
(lldb) po p
<FXPerson: 0x101857750>
複製代碼
④Xcode查看內存地址 debug->Debug Workflow->view memory 學習
有些操做可能用不到,讀者能夠自行拓展測試
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指令,能夠發現
a
、b
的ASCII編碼20
的十六進制Felix
180
sizeof——8
class_getInstanceSize——40
malloc_size——48
複製代碼
FXPerson
類中不聲明任何屬性
sizeof——8
class_getInstanceSize——8
malloc_size——16
複製代碼
Q1:爲何聲明屬性的先後會影響對象的內存排列呢?
Q2:sizeof、class_getInstanceSize、malloc_size分別是什麼?
Q3:不是說對象最少爲16字節,爲何class_getInstanceSize還能輸出8字節?
二進制重排——將最常常執行的代碼或最須要關鍵執行的代碼(如啓動階段的順序調用)聚合在一塊兒,將可有可無的代碼放在較低的優先級,造成一個更緊湊的__TEXT段
若是按照對象默認聲明的屬性順序進行內存分配,在進行屬性的8字節對齊時會浪費大量的內存空間,因此這裏系統會把對象的屬性從新排列,以此來最大化利用咱們的內存空間——與二進制重排有着殊途同歸之妙
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;
對象的屬性要內存對齊,而對象自己也須要進行內存對齊
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個格子
關於內存開闢,還有一個歷史遺留性問題——
alloc
在底層申請內存空間時調用了obj = (id)calloc(1, size)
。以前只有objc源碼
咱們無從下手,如今咱們能夠經過libmalloc源碼
來一探究竟
在libmalloc源碼
中新建target,按照objc源碼
中的方式調用
void *p = calloc(1, 40);
複製代碼
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
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 */
複製代碼
已知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->alloc
是default_zone_calloc
有時候打印也是閱讀源碼的一種方法——由打印可知實際調用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);
}
複製代碼
好不容易從malloc_zone_calloc
找到了default_zone_calloc
,而後又是熟悉的味道——zone->calloc(zone, num_items, size)
繼續打印試試
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
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
是怎麼來的
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字節對齊
對象是16字節對齊
關於寫文章,我喜歡先系統性的學一遍,將總體大綱寫下來,可是有可能在後續學習中會有新的感悟,會不定時將文章更新,但文章主要知識點都是正確無誤的
歡迎閱讀iOS探索系列