OC底層原理04 - 內(nèi)存對齊

獲取內(nèi)存大小的三種方式

  • sizeof
  • class_getInstanceSize
  • malloc_size

sizeof

sizeof是一個操作符,不是函數(shù)稼跳,一把用于計算內(nèi)存大小鸟雏。傳入的主要對象是數(shù)據(jù)類型(基本數(shù)據(jù)類型物臂、對象瘟滨、指針)锯蛀,這個在編譯器的編譯階段就會確定大小而不是在運行時。sizeof最終得到的結(jié)果是該數(shù)據(jù)類型占用空間的大小

class_getInstanceSize

這個方法在OC底層原理02 - alloc & init & new 源碼分析分析時就已經(jīng)分析了馅精,是runtime提供的api严嗜,用于獲取類的實例對象所占用的內(nèi)存大小,并返回具體的字節(jié)數(shù)洲敢,其本質(zhì)就是獲取實例對象中成員變量的內(nèi)存大小

malloc_size

這個函數(shù)是獲取系統(tǒng)實際分配的內(nèi)存大小

我們通過運行以下代碼驗證上述所說

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
        NSLog(@"objc對象類型占用的內(nèi)存大新:%lu", sizeof(objc));
        NSLog(@"objc對象實際占用的內(nèi)存大小:%lu", class_getInstanceSize([objc class]));
        NSLog(@"objc對象實際分配的內(nèi)存大新偌病:%lu", malloc_size((__bridge const void*)(objc)));
    }
    return 0;
}

運行結(jié)果如下
image.png

總結(jié)

sizeof

  • 計算類型占用的內(nèi)存大小称近,其中可以放基本數(shù)據(jù)類型對象哮塞、指針
  • 對于類似于int這樣的基本數(shù)據(jù)而言刨秆,sizeof獲取的就是數(shù)據(jù)類型占用的內(nèi)存大小,不同的數(shù)據(jù)類型所占用的內(nèi)存大小是不一樣的
  • 而對于類似于NSObject定義的實例對象而言忆畅,其對象類型的本質(zhì)就是一個結(jié)構(gòu)體(即 struct objc_object)的指針衡未,所以sizeof(objc)打印的是對象objc的指針大小,我們知道一個指針的內(nèi)存大小是8家凯,所以sizeof(objc) 打印是 8
  • 對于指針而言缓醋,sizeof打印的就是本身的內(nèi)存大小8

class_getInstanceSize

計算對象實際占用的內(nèi)存大小,這個需要依據(jù)類的屬性而變化绊诲,如果自定義類沒有自定義屬性送粱,僅僅只是繼承自NSObject,則類的實例對象實際占用的內(nèi)存大小是8

malloc_size

計算對象實際分配的內(nèi)存大小掂之,這個是由系統(tǒng)完成的抗俄,可以從上面的打印結(jié)果看出,實際分配的和實際占用的內(nèi)存大小并不相等世舰,這個問題可以通過OC底層原理02 - alloc & init & new 源碼分析中的16字節(jié)對齊算法來解釋這個問題

結(jié)構(gòu)體內(nèi)存對齊

首先我們定義兩個結(jié)構(gòu)體动雹,分別計算他們的內(nèi)存大小。

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

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

//計算 結(jié)構(gòu)體占用的內(nèi)存大小
NSLog(@"%lu-%lu", sizeof(HLstruct1), sizeof(HLstruct2));

輸出結(jié)果如下

image.png
從打印結(jié)果我們可以看出跟压,兩個結(jié)構(gòu)體定義的變量變量類型都是一致的胰蝠,唯一的不同是定義變量的順序不一致,但是他們所占用的內(nèi)存大小不相等卻不相同震蒋。這就是iOS中的內(nèi)存字節(jié)對齊

內(nèi)存對齊規(guī)則

每個特定平臺上的編譯器都有自己的默認“對齊系數(shù)”(也叫對齊模數(shù))茸塞。程序員可以通過預編譯命令#pragma pack(n),n = 1, 2, 4, 8, 16來改變這一系數(shù)喷好,其中的n就是你要指定的“對齊系數(shù)”翔横。在iOS中,Xcode默認為#pragma pack(8)梗搅,即8字節(jié)對齊

內(nèi)存字節(jié)對齊原則

數(shù)據(jù)成員對齊規(guī)則:struct 或者 union 的數(shù)據(jù)成員禾唁,第一個數(shù)據(jù)成員放在offset為0的地方效览,以后每個數(shù)據(jù)成員存儲的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員荡短,比如數(shù)據(jù)丐枉、結(jié)構(gòu)體等)的整數(shù)倍開始(例如int在32位機中是4字節(jié),則要從4的整數(shù)倍地址開始存儲)
數(shù)據(jù)成員為結(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ù)倍開始存儲)
結(jié)構(gòu)體的整體對齊規(guī)則:結(jié)構(gòu)體的總大小,即sizeof的結(jié)果泪掀,必須是其內(nèi)部最大成員的整數(shù)倍听绳,不足的要補齊

分析

下表是各數(shù)據(jù)類型在C和OC中所占內(nèi)存大小

image.png
根據(jù)內(nèi)存對齊原則以及各數(shù)據(jù)類型所占內(nèi)存大小,畫出上述兩個結(jié)構(gòu)體HLStruct1HLStruct2的內(nèi)存結(jié)構(gòu)示意圖
image.png

計算過程詳解:
HLStruct1:

  • 變量a:占8個字節(jié)异赫,從0開始椅挣,即0-7字節(jié)存儲變量a
  • 變量b:占1個字節(jié)塔拳,從8開始鼠证,即8字節(jié)儲存變量b
  • 變量c:占4個字節(jié),9不能整除4靠抑,故往后順移量九,直到12能整除4,所以從12開始颂碧,即12-15字節(jié)儲存變量c
  • 變量d:占2個字節(jié)娩鹉,從16開始,即16-17字節(jié)儲存變量d
    因此HLStruct1的需要的內(nèi)存大小為18字節(jié)稚伍,而HLStruct1最大變量的字節(jié)數(shù)為8HLStruct1實際的內(nèi)存大小必須是8的整數(shù)倍戚宦,18向上取整到24个曙,所以sizeof(HLStruct1)的結(jié)果是 24
    HLStruct2:
  • 變量a:占8個字節(jié),從0開始受楼,即0-7字節(jié)存儲變量a垦搬;
  • 變量b:占4個字節(jié),從8開始艳汽,即8-11字節(jié)儲存變量b
  • 變量c:占1個字節(jié)猴贰,從12開始,即12字節(jié)儲存變量c
  • 變量d:占2個字節(jié)河狐,13不能整除2米绕,故往后順移瑟捣,直到14能整除2,所以從14開始栅干,即14-15字節(jié)儲存變量d
    因此HLStruct2的需要的內(nèi)存大小為16字節(jié)迈套,而HLStruct2最大變量的字節(jié)數(shù)為8HLStruct2實際的內(nèi)存大小必須是8的整數(shù)倍碱鳞,16剛好為8的倍數(shù)桑李,所以sizeof(HLStruct2)的結(jié)果是 16

結(jié)構(gòu)體嵌套結(jié)構(gòu)體

上面的兩個結(jié)構(gòu)體只是簡單的定義數(shù)據(jù)成員,下面來一個比較復雜的窿给,結(jié)構(gòu)體中嵌套結(jié)構(gòu)體的內(nèi)存大小計算情況
定義一個結(jié)構(gòu)體HLStruct3贵白,在HLStruct3中嵌套HLStruct2,如下所示

struct HLStruct3 {
    double a;
    int b;
    char c;
    short d;
    int e;
    struct HLStruct2 f;
}struct3;

輸出

NSLog(@"%lu-%lu", sizeof(struct3), sizeof(struct3.f));

輸出結(jié)果如下

image.png

struct3內(nèi)存計算

  • 變量a:占8個字節(jié)崩泡,從0開始禁荒,即0-7字節(jié)存儲變量a
  • 變量b:占4個字節(jié)允华,從8開始圈浇,即8-11字節(jié)儲存變量b
  • 變量c:占1個字節(jié),從12開始靴寂,即12字節(jié)儲存變量c
  • 變量d:占2個字節(jié)磷蜀,13不能整除2,故往后順移百炬,直到14能整除2褐隆,所以從14開始,即14-15字節(jié)儲存變量d
  • 變量e:占4個字節(jié)剖踊,從16開始庶弃,即16-19字節(jié)儲存變量e
  • 變量f:占16個字節(jié),f是一個結(jié)構(gòu)體德澈,根據(jù)內(nèi)存對齊原則二歇攻,結(jié)構(gòu)體成員要從其內(nèi)部最大成員大小的整數(shù)倍開始存儲,而HLStruct2中最大的成員大小為8梆造,所以f要從8的整數(shù)倍開始缴守,當前是從20開始,所以不符合要求镇辉,需要往后移動到24屡穗,24是8的整數(shù)倍,符合內(nèi)存對齊原則忽肛,即24-39存儲變量f
    因此HLStruct3的需要的內(nèi)存大小為40字節(jié)村砂,而HLStruct3最大變量的字節(jié)數(shù)為8HLStruct3實際的內(nèi)存大小必須是8的整數(shù)倍屹逛,40剛好為8的倍數(shù)础废,所以sizeof(HLStruct3)的結(jié)果是 40
    image.png

二次驗證

在定義一個結(jié)構(gòu)體汛骂,如下所示

struct HLStruct4 {
    short a;
    double b;
}struct4;
struct HLStruct5 {
    char a;
    int b;
    struct HLStruct4 c;
}struct5;

HLStruct4內(nèi)存計算

  • 變量a:占2個字節(jié),從0開始色迂,即0-2字節(jié)存儲變量a香缺;
  • 變量b:占8個字節(jié),3不能整除8歇僧,故往后順移图张,直到8能整除8,所以從8開始诈悍,即8-15字節(jié)存儲變量b祸轮;
    因此HLStruct4的需要的內(nèi)存大小為16字節(jié),而HLStruct4最大變量的字節(jié)數(shù)為8侥钳,HLStruct4實際的內(nèi)存大小必須是8的整數(shù)倍适袜,16剛好為8的倍數(shù),所以sizeof(HLStruct4)的結(jié)果是 16
    HLStruct5內(nèi)存計算
  • 變量a:占1個字節(jié)舷夺,從0開始苦酱,即0字節(jié)存儲變量a
  • 變量b:占2個字節(jié)给猾,1不能整除2疫萤,故往后順移,直到2能整除2敢伸,所以從2開始扯饶,即2-3字節(jié)存儲變量b
  • 變量c:占16個字節(jié)池颈,HLStruct4中最大的成員大小為8尾序,所以c要從8的整數(shù)倍開始多矮,當前是從4開始潜必,所以不符合要求,需要往后移動到8嗡害,8是8的整數(shù)倍琢歇,符合內(nèi)存對齊原則脯爪,即8-23存儲變量c
    因此HLStruct5的需要的內(nèi)存大小為24字節(jié),而HLStruct4最大變量的字節(jié)數(shù)為8矿微,HLStruct5實際的內(nèi)存大小必須是8的整數(shù)倍24剛好為8的倍數(shù)尚揣,所以sizeof(HLStruct5)的結(jié)果是 24
    下圖為輸出及打印
    image.png

內(nèi)存優(yōu)化(屬性重排)

根據(jù)內(nèi)存對齊原則涌矢,HLStruct1補齊了9個字節(jié),而HLStruct2只補齊1個字節(jié)即可滿足該規(guī)則快骗,因此得出一個結(jié)論結(jié)構(gòu)體內(nèi)存大小與結(jié)構(gòu)體成員內(nèi)存大小的順序有關(guān)娜庇。

創(chuàng)建一個對象來探索

  1. 首先定義一個自定義類HLPerson類塔次,并添加幾個屬性
@interface HLPerson : NSObject

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

@property (nonatomic) char c1;
@property (nonatomic) char c2;

@end
  1. 在main中創(chuàng)建HLPerson的實例對象,并對其屬性賦值
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        HLPerson *person = [HLPerson alloc];
        person.name      = @"HL";
        person.nickName  = @"Lay";
        person.age       = 17;
        person.height    = 180;
        person.c1        = 'a';
        person.c2        = 'b';

        NSLog(@"%@", person);
    }
    return 0;
}
  1. 斷點調(diào)試person
  • x personxmemory read的簡寫名秀,讀取內(nèi)存信息
    tips:iOS是小端模式励负,所以內(nèi)存的讀取要反著讀,即cd 83 00 00 01 80 1d 00 應(yīng)該讀取為 0x001d8001000083cd
    image.png
  • x/8gx person:以16進制打印8行內(nèi)存信息匕得,并分別打印其指向继榆,此方法讀取地址更為便捷
    image.png

    這里雖然打印了8行內(nèi)存信息,但實際上person對象變量并沒有使用這么多內(nèi)存汁掠,可以通過class_getInstanceSize方法獲取實際上該對象只占用了40字節(jié)的內(nèi)存略吨,也就是上圖中前五段內(nèi)存,所以后三段全都為0x00000000考阱,但是有幾個屬性值卻并沒有找到翠忠。
    分析
    沒有找到agec1c2對應(yīng)的值乞榨,是不是蘋果做了什么處理避免內(nèi)存過度消耗秽之,我們用沒有正常輸出信息的內(nèi)存嘗試解析下
    image.png

    結(jié)論
    namenickname吃既、height都是各自占用8字節(jié)考榨。可以直接打印出來态秧;而ageInt占用4字節(jié)董虱,c1c2char,各自占用1字節(jié)申鱼。我們推測系統(tǒng)可能進行屬性重排愤诱,將他們存放在了一個塊區(qū)。
    下圖是HLPerson的內(nèi)存分布情況
    image.png

    特殊的doublefloat
    height屬性類型修改為double
//@property (nonatomic, assign) long height;
@property (nonatomic, assign) double height;

重新運行

image.png

直接po打印0x4066800000000000捐友,并不能正確輸出變量height的值淫半,這是因為編譯器po打印默認當做int類型處理

  • p/x (double)180:將180轉(zhuǎn)成double類型然后以16進制進行打印匣砖,發(fā)現(xiàn)地址完全一樣科吭。
    image.png
    height改成float類型也可以用p/x (float)180驗證
    封裝2個驗證函數(shù)
// float轉(zhuǎn)換為16進制
void hl_float2HEX(float f) {
    union uuf { float f; char s[4]; } uf;
    uf.f = f;
    printf("0x");
    for (int i = 3; i >= 0; i--) {
        printf("%02x", 0xff & uf.s[i]);
    }
    printf("\n");
}

// double轉(zhuǎn)換為16進制
void hl_double2HEX(double d) {
    union uud { double d; char s[8]; } ud;
    ud.d = d;
    printf("0x");
    for (int i = 7; i >= 0; i--) {
        printf("%02x", 0xff & ud.s[i]);
    }
    printf("\n");
}

打印驗證


image.png

字節(jié)對齊到底采用多少字節(jié)對齊?

objc4源碼中搜索class_getInstanceSize猴鲫,可以在runtime.h找到:

/** 
 * Returns the size of instances of a class.
 * 
 * @param cls A class object.
 * 
 * @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
 */
OBJC_EXPORT size_t
class_getInstanceSize(Class _Nullable cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

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

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

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

// 其中 WORD_MASK 為
#   define WORD_MASK 7UL

通過源碼可以看出对人,對于一個對象來說,其真正的對齊方式是8字節(jié)對齊拂共,8字節(jié)對齊已經(jīng)足夠滿足對象的需求了
總結(jié)
class_getInstanceSize:是采用8字節(jié)對齊牺弄,參照的對象的屬性內(nèi)存大小
malloc_size:采用16字節(jié)對齊,參照的整個對象的內(nèi)存大小宜狐,對象實際分配的內(nèi)存大小必須是16的整數(shù)倍

內(nèi)存對齊算法

至此势告,我們已知的16字節(jié)對齊算法有兩種

  • alloc源碼分析中的align16
  • malloc源碼分析中的segregated_size_to_fit

align16

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

segregated_size_to_fit

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

算法原理:k + (16 - 1) >> 4 << 4 蛇捌,其中右移4+左移4相當于將后4位抹零,跟k / 16 * 16一樣 咱台,小于16就成0了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末络拌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子回溺,更是在濱河造成了極大的恐慌春贸,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件馅而,死亡現(xiàn)場離奇詭異祥诽,居然都是意外死亡,警方通過查閱死者的電腦和手機瓮恭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門雄坪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人屯蹦,你說我怎么就攤上這事维哈。” “怎么了登澜?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵阔挠,是天一觀的道長。 經(jīng)常有香客問我脑蠕,道長购撼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任谴仙,我火速辦了婚禮迂求,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘晃跺。我一直安慰自己揩局,他們只是感情好,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布掀虎。 她就那樣靜靜地躺著凌盯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪烹玉。 梳的紋絲不亂的頭發(fā)上驰怎,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天,我揣著相機與錄音二打,去河邊找鬼砸西。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的芹枷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼莲趣,長吁一口氣:“原來是場噩夢啊……” “哼鸳慈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起喧伞,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤走芋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后潘鲫,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翁逞,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年溉仑,在試婚紗的時候發(fā)現(xiàn)自己被綠了挖函。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡浊竟,死狀恐怖怨喘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情振定,我是刑警寧澤必怜,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站后频,受9級特大地震影響梳庆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜卑惜,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一膏执、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧残揉,春花似錦胧后、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至镇草,卻和暖如春眶痰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背梯啤。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工竖伯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓七婴,卻偏偏與公主長得像祟偷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子打厘,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

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