iOS底層探索之內(nèi)存對齊和calloc

之前通過 objc 的源碼探索了 alloc 的內(nèi)部流程,到最后會調(diào)用 size = cls->instanceSize(extraBytes); 方法脆霎,獲取內(nèi)存大小,但是這個大小到底是怎么計算的呢?

獲取大小后框往,會調(diào)用 calloc(1, size) 方法開辟內(nèi)存大小,開辟的時候又有什么不同呢知允?

這次就繼續(xù)探索一下系統(tǒng)的內(nèi)存分配撒蟀。

一、屬性所占內(nèi)存計算

從應(yīng)用代碼開始

@interface GLPerson : NSObject
// 會有一個隱藏屬性 isa  占8個字節(jié)
@property (nonatomic, copy  ) NSString *name; // 8
@property (nonatomic, assign) int height; // 4
@property (nonatomic, assign) char char1; // 1
@property (nonatomic, assign) char char2; // 1

@en

---
        GLPerson *p = [[GLPerson alloc] init];
        p.height = 180;
        p.name = @"loong";
        p.char1 = 'g';
        p.char2 = 'n';
        NSLog(@"%zd %zd", class_getInstanceSize([GLPerson class]), malloc_size((__bridge const void *)(p)));

上面會輸出:24 32

在alloc流程中簡單說過温鸽,屬性內(nèi)存分配的時候是8字節(jié)對齊保屯,GLPerson類的實例所占大小計算為 8 (isa) + 8 (name) + 4 (height) + 1 (char1) + 1 (char1) == 22。(模擬計算結(jié)果涤垫,實際在跟源碼的時候蘋果有內(nèi)存優(yōu)化配椭,會把 4 (height) + 1 (char1) + 1 (char1) 放到一個8字節(jié)里面存儲,這樣避免了浪費)

然后會對22進(jìn)行8字節(jié)對齊雹姊,得到的是24股缸。

// 1 
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
 // 2
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}
// 3
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}
// 4
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

根據(jù)上面4個方法的調(diào)用順序

  1. class_getInstanceSize : 內(nèi)部調(diào)用 alignedInstanceSize 返回
  2. alignedInstanceSize : 內(nèi)部調(diào)用 word_align(unalignedInstanceSize()) 返回
  3. unalignedInstanceSize : 內(nèi)部調(diào)用 data()->ro()->instanceSize返回,data()ro()的數(shù)據(jù)會在 loadImage 的時候完成吱雏,instanceSize會根據(jù)類有多少屬性敦姻,返回已經(jīng)經(jīng)過編譯器優(yōu)化存儲后的結(jié)果
  4. word_align : 這個會對實例大小做8字節(jié)對齊,會返回8的倍數(shù)大小
1.1 對齊計算

word_align 就是8字節(jié)對齊計算

#   define WORD_MASK 7UL

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

可以看出word_align的對齊計算是 (x + 7) & ~7歧杏,

8字節(jié)對齊計算方式.png
1.2 計算結(jié)構(gòu)體大小

咱們知道OC繼承與C镰惦,并且 struct objc_class : objc_objectobjc_class 的源碼也是一個結(jié)構(gòu)體犬绒,結(jié)構(gòu)體在計算大小的時候有3個原則:

1旺入、每個成員的偏移量都必須是當(dāng)前成員所占內(nèi)存大小的整數(shù)倍如果不是編譯器會在成員之間加上填充字節(jié)。
2凯力、結(jié)構(gòu)體作為成員: 如果?個結(jié)構(gòu)?有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最?元素??的整數(shù)倍地址開始存儲茵瘾。(struct a?存有struct b,b?有char,int ,double等元素,那b應(yīng)該從8的整數(shù)倍開始存儲)
3、當(dāng)所有成員大小計算完畢后咐鹤,編譯器判斷當(dāng)前結(jié)構(gòu)體大小是否是結(jié)構(gòu)體中最寬的成員變量大小的整數(shù)倍 如果不是會在最后一個成員后做字節(jié)填充拗秘。

struct structA {
    long height;    // 8
    int age;        // 4
    char char1;     // 1
    short short1;   // 2
};

struct structB {
    int age;        // 4
    long height;    // 8
    char char1;     // 1
    short short1;   // 2
};

struct structC {
    int age;        // 4
    struct structB sb;
    char sex; // 1
};
---
        struct structA a = {
            12, 20, 'a', 123
        };
        struct structB b = {};
        struct structC c = {};
        
        NSLog(@"A:%lu, B:%lu, C:%lu", sizeof(a), sizeof(b), sizeof(c));
---
console: A:16, B:24, C:40

structA:
height 0-->7;
age 8 --> 11;
char1 12;
short1 (根據(jù)原則1,第13位不是2的整數(shù)倍祈惶,往后移雕旨,14滿足) 14-->15;
實際總共:0--> 15 為 16,
再根據(jù)原則3捧请,需要是8的倍數(shù)凡涩,16滿足,最后就是16疹蛉。

structB:
age 0 --> 3;
height (根據(jù)原則1活箕,4不滿足8的整數(shù)倍,往后移氧吐,8滿足) 8-->15;
char1 16;
short1 (第17位不是2的整數(shù)倍讹蘑,往后移末盔,18滿足)18-->19;
實際總共:0--> 19 為 20,
再根據(jù)原則3座慰,需要是8的倍數(shù)陨舱,所以最后是24。

structC:
age 0 --> 3;
structB sb (根據(jù)原則2版仔,因為structB里面最大的是8字節(jié)游盲,4不滿足8的整數(shù)倍,往后移蛮粮,8滿足益缎,可知structB占24) 8-->31;
sex 32;
實際總共:0--> 32 為 33,
再根據(jù)原則3然想,需要是8的倍數(shù)莺奔,所以最后是40。

1.3 編譯優(yōu)化

測試如下代碼

struct structB {
    long isa;       // 8
    char char1;     // 1
    int height;     // 4
    char char2;     // 1
    double name;    // 8
    char char3;     // 1
    char char4;     // 1
};
---
@interface GLPerson : NSObject
// 默認(rèn)屬性isa  占8個字節(jié)
@property (nonatomic, assign) char char1;       // 1
@property (nonatomic, assign) int height;       // 4
@property (nonatomic, assign) char char2;       // 1
@property (nonatomic, copy  ) NSString *name;   // 8
@property (nonatomic, assign) char char3;       // 1
@property (nonatomic, assign) char char4;       // 1
@end
---
        struct structB b = {};

        LGPerson *p = [[LGPerson alloc] init];
        p.char1 = 'a';
        p.height = 180;
        p.char2 = 'b';
        p.name = @"loong";
        p.char3 = 'c';
        p.char4 = 'd';

NSLog(@"%lu, %zd, %zd",sizeof(b), class_getInstanceSize([LGPerson class]), malloc_size((__bridge const void *)(p)));

console: 40, 24, 32

通過輸出結(jié)果可以知道变泄,structB大小為40令哟,LGPerson真正占用大小為24。為什么妨蛹,這里面就是系統(tǒng)底層做了編譯優(yōu)化屏富,在字節(jié)對齊的基礎(chǔ)上,又節(jié)省了空間蛙卤『莅耄可以通過打印內(nèi)存地址查看

可以使用x 或者 memory read命令查看某個對象的內(nèi)存情況。

x-memory read.png

更方便的查看4xg規(guī)則

4xg.png

可以通過po命令打印出內(nèi)存中對應(yīng)的值如下:

內(nèi)存優(yōu)化排布.png

可以知道底層把 height char1 char2 char3 char4 放到了一個8字節(jié)里面颤难。
這樣不會像結(jié)構(gòu)體那樣按順序存儲神年,中間會有很多補位。在保證對齊原則的情況下乐严,極大的節(jié)省了內(nèi)存空間瘤袖。

二衣摩、calloc() 源碼分析

分析 malloc 的源碼昂验,官方地址,本次分析的是libmalloc-283.100.6版本艾扮。

        void *p = calloc(1, 40);
        NSLog(@"%lu",malloc_size(p));

console: 48

為什么開辟了48既琴??泡嘴?從源碼里面找下答案

分析 malloc 源碼的時候甫恩,還是需要配置一個能編譯運行的源碼工程的,這樣斷點能走進(jìn)去酌予,方便分析磺箕。

2.1 calloc

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;
}

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;
}

calloc -> malloc_zone_calloc -> ptr = zone->calloc(zone, num_items, size)

發(fā)現(xiàn) calloc 又調(diào)回去了奖慌??松靡?只看源碼的話简僧,確實找不到下一步走向了哪里。

還是需要編譯運行工程雕欺,通過調(diào)用 void *p = calloc(1, 40);ptr = zone->calloc(zone, num_items, size) 打個斷點岛马。然后打印下,看看調(diào)用方法

斷點zone->calloc下步調(diào)用.png

也可以通過按住 control 點擊 step into 多次屠列,進(jìn)入下一個方法調(diào)用啦逆。

可知下一步來到了 default_zone_calloc

2.2 default_zone_calloc 、nano_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);
}

同樣的方式笛洛,來到 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);
}

接著會走到 _nano_malloc_check_clear

2.3 _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;
      // 在此處segregated_size_to_fit進(jìn)行16字節(jié)對齊
    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));
        // 通過斷點發(fā)現(xiàn)pMeta為0x0, ptr為NULL夏志,會走到else里面
    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;
}

這個時候我發(fā)現(xiàn),size竟然變成了18

first_invoke.png

通過線程可以看到第一次進(jìn)來的調(diào)用順序

_malloc_initialize_once調(diào)用.jpg

發(fā)現(xiàn)是調(diào)用了 _malloc_initialize_once 方法苛让。這個先跳過盲镶,

然后繼續(xù)往下走

_nano_malloc_check_clear-1.png

會走到 ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index); 這個方法里面就是查找能開辟給定 slot_bytes 大小的內(nèi)存的地方。直到查到返回蝌诡。

static MALLOC_INLINE void *
segregated_next_block(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
{
    while (1) {
        uintptr_t theLimit = pMeta->slot_limit_addr; // Capture the slot limit that bounds slot_bump_addr right now
        uintptr_t b = OSAtomicAdd64Barrier(slot_bytes, (volatile int64_t *)&(pMeta->slot_bump_addr));
        b -= slot_bytes; // Atomic op returned addr of *next* free block. Subtract to get addr for *this* allocation.

        if (b < theLimit) {   // Did we stay within the bound of the present slot allocation?
            return (void *)b; // Yep, so the slot_bump_addr this thread incremented is good to go
        } else {
            if (pMeta->slot_exhausted) { // exhausted all the bands availble for this slot?
                pMeta->slot_bump_addr = theLimit;
                return 0;                // We're toast
            } else {
                // One thread will grow the heap, others will see its been grown and retry allocation
                _malloc_lock_lock(&nanozone->band_resupply_lock[mag_index]);
                // re-check state now that we've taken the lock
                if (pMeta->slot_exhausted) {
                    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                    return 0; // Toast
                } else if (b < pMeta->slot_limit_addr) {
                    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                    continue; // ... the slot was successfully grown by first-taker (not us). Now try again.
                } else if (segregated_band_grow(nanozone, pMeta, slot_bytes, mag_index)) {
                    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                    continue; // ... the slot has been successfully grown by us. Now try again.
                } else {
                    pMeta->slot_exhausted = TRUE;
                    pMeta->slot_bump_addr = theLimit;
                    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                    return 0;
                }
            }
        }
    }
}

那什么時候進(jìn)行的16字節(jié)對齊的呢溉贿?

發(fā)現(xiàn)在查找地址之前有個方法 size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); 這個方法返回后,slot_bytes就成了48了(16字節(jié)對齊過了)

2.4 segregated_size_to_fit -- 16字節(jié)對齊

16字節(jié)對齊方法

#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;
}
16字節(jié)對齊計算.png

通過計算發(fā)現(xiàn)浦旱,和上面講的8字節(jié)對齊是不是道理一樣宇色,先給你補個差額(15),
然后通過右移4位颁湖,把 24 以下的二進(jìn)制位干掉宣蠕,
再左移4位,恢復(fù)原來的高二進(jìn)制位的數(shù)據(jù)甥捺。
從而達(dá)到16字節(jié)對齊


END

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抢蚀,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子镰禾,更是在濱河造成了極大的恐慌皿曲,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吴侦,死亡現(xiàn)場離奇詭異屋休,居然都是意外死亡,警方通過查閱死者的電腦和手機备韧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門劫樟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事叠艳∧坛拢” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵附较,是天一觀的道長尿瞭。 經(jīng)常有香客問我,道長翅睛,這世上最難降的妖魔是什么声搁? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮捕发,結(jié)果婚禮上疏旨,老公的妹妹穿的比我還像新娘。我一直安慰自己扎酷,他們只是感情好檐涝,可當(dāng)我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著法挨,像睡著了一般谁榜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上凡纳,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天窃植,我揣著相機與錄音,去河邊找鬼荐糜。 笑死巷怜,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的暴氏。 我是一名探鬼主播延塑,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼答渔!你這毒婦竟也來了关带?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤沼撕,失蹤者是張志新(化名)和其女友劉穎宋雏,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體端朵,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡好芭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了冲呢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡招狸,死狀恐怖敬拓,靈堂內(nèi)的尸體忽然破棺而出邻薯,到底是詐尸還是另有隱情,我是刑警寧澤乘凸,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布厕诡,位于F島的核電站,受9級特大地震影響营勤,放射性物質(zhì)發(fā)生泄漏灵嫌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一葛作、第九天 我趴在偏房一處隱蔽的房頂上張望寿羞。 院中可真熱鬧,春花似錦赂蠢、人聲如沸绪穆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玖院。三九已至,卻和暖如春第岖,著一層夾襖步出監(jiān)牢的瞬間难菌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工蔑滓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扔傅,地道東北人。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓烫饼,卻偏偏與公主長得像猎塞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子杠纵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容