iOS的OC對象的內存對齊

前言

經過上一篇文章iOS的OC對象建立的alloc原理的介紹能夠很清楚了對象的建立在底層的過程是怎樣的了。而且只簡單介紹了對象的開闢內存的空間,這篇文章將會詳細介紹一下對象的內存對齊算法

爲了方便對下面的內容介紹,用TestObject做爲例子,示例代碼以下:express

@interface TestObject : NSObject

@property(nonatomic,copy) NSString *name;
@property(nonatomic,assign) int age;
@property(nonatomic,assign) long height;
@property(nonatomic,copy) NSString *hobby;

@property(nonatomic,assign) int sex;
@property(nonatomic,assign) char char1;
@property(nonatomic,assign) char char2;

@end
複製代碼

1.對象的內存狀況

查看對象的內存狀況能夠在lldb中用xppo這些指令來查看數組

從中能夠看到的 0x101a5e050是對象的指針的首地址,每一行的開頭部分都是這一行的內存值的開始的排列。而 0x0000001300006261這部分的是內存的值,這些內存的值都是從對象的首地址來依次的排列的。

p,是expression - 的別名,p爲print的簡寫,同時能夠寫爲pri,打印某個東西,能夠i是變量和表達式;call爲調用某個方法,輸出變量也是能夠的。 po通常用於打印對象,是expression -O — 的別名。 p 和 po 的區別在於使用 po 只會輸出對應的值,而 p 則會返回值的類型以及命令結果的引用名。 bash

從中示例能夠看到第一個打印的是一串數字,po出來是有問題,其餘的均可以正常地從內存地址打印出來的,這時咱們能夠換一種方式打印post

0x0000001300006261拆分出來打印就能夠獲得了,其中 9798分別是小寫字母a和b的ASCII編碼。爲何會這樣呢?這裏面就涉及到了對象的內存優化了,在上一篇文章中,有介紹到內存是以8字節來分配的。其中TestObject中的 ageint佔4字節, char1char2char分別佔1個字節,若是都按8字節來分的話就會形成很大的浪費。至於爲何會這樣的,就由下面的 內存對齊來介紹了。

2 內存對齊

先說一下內存對齊的原則:優化

1.數據成員對齊規則:結構體(struct)或者聯合體(union)的數據成員,第一個數據成員放在offset爲0的地方,之後每一個數據成員存儲的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員,好比說是數組,結構體等)的整數倍開始(好比int爲4字節,則要從4的整數倍地址開始存儲)。ui

2.結構體做爲成員:若是一個結構體裏有某些結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始存儲編碼

3.收尾工做:結構體的總大小,也就是sizeof的結果,必須是其內部最大成員的整數倍,不足的要補齊。atom

經過上面的說明是否是以爲不是很明白?下面就舉個例子來解釋一下spa

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

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

2020-05-02 22:23:48.415188+0800 LGTest[9394:360173] 24====16
複製代碼

下面就是根據這個例子來進行解釋。首先須要遵循一個min算法,假設min(m,n),其中m爲當前開始的位置,n爲大小。根據內存對齊的原則,m是n的整數倍的狀況下才開始排,若是不是那麼就m的值往上加直到是n的整數倍才能夠排。例如上面的例子:JSStruct1中的char a的大小是1,開始的位置是0,那麼min(0,1),是能夠排的;b的大小是8,開始的位置是1,min(1,8),因此b的m須要到8才能夠排,那麼b的是須要從8開始排的,那麼排b是8,9,10,11,12,13,14,15,即到15將b排完;排完b以後到了c,此時c的開始位置應該爲16,大小爲4,min(16,4),由於16是4的整數倍,那麼c的排位是16,17,18,19,排完c以後的位置是到了19;那麼d的開始位置爲20,大小爲2,min(20,2),由於20是2的整數倍,那麼排位d的是20,21,排完d的位置是21。由於總體的內存對齊是8字節對齊的。須要8的倍數因此最終是24,這就是JSStruct1總體的內存大小,那麼你能夠對JSStruct2來根據上面的解釋進行練習一下爲何是16。

可是相對於結構體的內存對齊是按照屬性排下來,對象的內存對齊卻不是的,由於作了內存由編譯器作了優化(由第一部分的內容能夠看到)。

3.對象生成的內存與系統開闢的內存關係

仍是經過TestObject的例子,而後將兩個char類型的char1和char2和一個int類型的sex不實現而且TestObject類中註釋掉

TestObject *test = [TestObject alloc];
        test.name = @"jason";
        test.age = 19;
        test.hobby = @"足球";
        test.height = 180;
//        test.sex = 1;
//        test.char1 = 'a';
//        test.char2 = 'b';
        
NSLog(@"對象生成的內存:%lu,系統開闢的內存:%lu",class_getInstanceSize([test class]),malloc_size((__bridge const void *)(test)));

2020-05-02 22:39:10.191590+0800 LGTest[9577:368352] 對象生成的內存:40,系統開闢的內存:48
複製代碼

由上面的結果知道,爲何對象生成的內存和系統開闢的內存是不同的呢? 爲了搞清楚仍是須要用到上一篇文章iOS的OC對象建立的alloc原理裏面源碼的_class_createInstanceFromZone方法的源碼

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance //判斷當前class或者superclass是否有.cxx_construct構造方法的實現 bool hasCxxCtor = cls->hasCxxCtor(); //判斷當前class或者superclass是否有.cxx——destruct析構方法的實現 bool hasCxxDtor = cls->hasCxxDtor(); bool fast = cls->canAllocNonpointer(); //經過進行內存對齊獲得實例大小 size_t size = cls->instanceSize(extraBytes); if (outAllocatedSize) *outAllocatedSize = size; id obj; if (!zone && fast) { obj = (id)calloc(1, size); if (!obj) return nil; //初始化實例的isa指針 obj->initInstanceIsa(cls, hasCxxDtor); } else { if (zone) { obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size); } else { obj = (id)calloc(1, size); } if (!obj) return nil; // Use raw pointer isa on the assumption that they might be // doing something weird with the zone or RR. obj->initIsa(cls); } if (cxxConstruct && hasCxxCtor) { obj = _objc_constructOrFree(obj, cls); } return obj; } 複製代碼

經過斷點能夠看到

size_t size = cls->instanceSize(extraBytes);
複製代碼

返回的對象內存大小是40,往下走到了calloc方法,可是直接進去是進不去的由於這是libmalloc的源碼也是能夠直接到蘋果的開源庫下載源碼。由於objc的源碼和malloc的源碼是分開的,直接按下面的方式來直接進去。

經過源碼的一步步跳轉到了如下這種狀況下,看到 ptr = zone->calloc(zone, num_items, size);若是是一直這樣點擊源碼跳轉下去就變成了死循環,確定是有問題的。

爲了解決這個問題能夠用 po命令來

而後再一步一步來搜索出來,再用 po命令和斷點相結合就能夠層層深刻進去源碼

最終找到在計算開闢系統內存大小的源碼的方法在

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

#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)
複製代碼

從源碼中看到,傳進來的size是40,SHIFT_NANO_QUANTUM是4,NANO_REGIME_QUANTA_SIZE就是16,那麼這裏就是16字節對齊,由於傳進來的是40,爲了可以16字節對齊須要補齊因此獲得的就是48。從上面的對象的字節對齊是8字節,爲何系統開闢的內存是16字節呢?由於8字節對齊的參考的是對象裏面的屬性,而16字節對齊的參考的是整個對象,由於系統開闢的內存若是隻是按照對象屬性的大小來的話,可能會致使內存溢出的。

4.問題

定義一個類TestJason,裏面什麼屬性都沒有,根據上面介紹的內存對齊,獲得的對象內存和系統的內存分別是多少呢?

TestJason *test2 = [TestJason alloc];
NSLog(@"%lu===%lu",class_getInstanceSize([test2 class]),malloc_size((__bridge const void *)(test2)));
複製代碼

答案是:8和16

這是爲何呢?有的人會認爲是16和16這個答案,由於上一篇文章介紹alloc原理的時候有說過最少分配是16字節。可是經過看class_getInstanceSize的源碼,知道里面其實8字節對齊就直接返回了。

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

// May be unaligned depending on class's ivars. uint32_t unalignedInstanceSize() { assert(isRealized()); return data()->ro->instanceSize; } // Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
  return word_align(unalignedInstanceSize());
}
複製代碼

5.最後

至此OC對象的內存對齊的介紹就到這裏了,後續還會陸續出一些其餘的底層知識,歡迎關注。若是以爲內容有出錯的,歡迎評論留言。

相關文章
相關標籤/搜索