iOS-底層原理 05:內存對齊原理

iOS 底層原理 文章匯總

在探討內存對齊原理之前,首先介紹下iOS中獲取內存大小的三種方式

獲取內存大小的三種方式

獲取內存大小的三種方式分別是:

  • sizeof
  • class_getInstanceSize
  • malloc_size

sizeof

  • 1、sizeof是一個操作符,不是函數
  • 2、我們一般用sizeof計算內存大小時,傳入的主要對象是數據類型,這個在編譯器的編譯階段(即編譯時)就會確定大小而不是在運行時確定。
  • 3、sizeof最終得到的結果是該數據類型占用空間的大小

class_getInstanceSize

這個方法在iOS-底層原理 02:alloc & init & new 源碼分析分析時就已經分析了,是runtime提供的api,用于獲取類的實例對象所占用的內存大小,并返回具體的字節數,其本質就是獲取實例對象中成員變量的內存大小

malloc_size

這個函數是獲取系統實際分配的內存大小

可以通過下面代碼的輸出結果來驗證我們上面的說法

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

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

以下是打印結果


三種獲取內存大小的打印結果

總結

  • sizeof:計算類型占用的內存大小,其中可以放 基本數據類型、對象、指針

    • 對于類似于int這樣的基本數據而言,sizeof獲取的就是數據類型占用的內存大小,不同的數據類型所占用的內存大小是不一樣的

    • 而對于類似于NSObject定義的實例對象而言,其對象類型的本質就是一個結構體(即 struct objc_object)的指針,所以sizeof(objc)打印的是對象objc的指針大小,我們知道一個指針的內存大小是8,所以sizeof(objc) 打印是 8。注意:這里的8字節與isa指針一點關系都沒有!!!)

    • 對于指針而言,sizeof打印的就是8,因為一個指針的內存大小是8,

  • class_getInstanceSize:計算對象實際占用的內存大小,這個需要依據類的屬性而變化,如果自定義類沒有自定義屬性,僅僅只是繼承自NSObject,則類的實例對象實際占用的內存大小是8,可以簡單理解為8字節對齊

  • malloc_size:計算對象實際分配的內存大小,這個是由系統完成的,可以從上面的打印結果看出,實際分配的和實際占用的內存大小并不相等,這個問題可以通過iOS-底層原理 02:alloc & init & new 源碼分析中的16字節對齊算法來解釋這個問題

結構體內存對齊

接下來,我們首先定義兩個結構體,分別計算他們的內存大小,以此來引入今天的正題:內存對齊原理

//1、定義兩個結構體
struct Mystruct1{
    char a;     //1字節
    double b;   //8字節
    int c;      //4字節
    short d;    //2字節
}Mystruct1;

struct Mystruct2{
    double b;   //8字節
    int c;      //4字節
    short d;    //2字節
    char a;     //1字節
}Mystruct2;

//計算 結構體占用的內存大小
NSLog(@"%lu-%lu",sizeof(Mystruct1),sizeof(Mystruct2));

以下是輸出結果


image

從打印結果我們可以看出一個問題,兩個結構體乍一看,沒什么區別,其中定義的變量 和 變量類型都是一致的,唯一的區別只是在于定義變量的順序不一致,那為什么他們做占用的內存大小不相等呢?其實這就是iOS中的內存字節對齊現象

內存對齊規則

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

一般內存對齊的原則主要有3點,可以回看iOS-底層原理 02:alloc & init & new 源碼分析中的說明,

可以將內存對齊原則可以理解為以下兩點:

  • 【原則一】 數據成員的對齊規則可以理解為min(m, n) 的公式, 其中 m表示當前成員的開始位置, n表示當前成員所需要的位數。如果滿足條件 m 整除 n (即 m % n == 0), nm 位置開始存儲, 反之繼續檢查 m+1 能否整除 n, 直到可以整除, 從而就確定了當前成員的開始位置。
  • 【原則二】數據成員為結構體:當結構體嵌套了結構體時,作為數據成員的結構體的自身長度作為外部結構體的最大成員的內存大小,比如結構體a嵌套結構體b,b中有char、int、double等,則b的自身長度為8
  • 【原則三】最后結構體的內存大小必須是結構體中最大成員內存大小的整數倍,不足的需要補齊。

驗證對齊規則

下表是各種數據類型在ios中的占用內存大小,根據對應類型來計算結構體中內存大小


數據類型對應的字節數表格

我們可以通過下圖圖來說明下為什么兩個結構體MyStruct1 & MyStruct2的內存大小打印不一致的情況,如圖所示

結構體對應的存儲情況

結構體MyStruct1 內存大小計算

根據內存對齊規則計算MyStruct1的內存大小,詳解過程如下:

  • 變量a:占1個字節,從0開始,此時min(0,1),即 0 存儲 a
  • 變量b:占8個字節,從1開始,此時min(1,8),1不能整除8,繼續往后移動,知道min(8,8),從8開始,即 8-15 存儲 b
  • 變量c:占4個字節,從16開始,此時min(16,4),16可以整除4,即 16-19 存儲 c
  • 變量d:占2個字節,從20開始,此時min(20, 2),20可以整除2,即20-21 存儲 d

因此MyStruct1的需要的內存大小為 15字節,而MyStruct1中最大變量的字節數為8,所以 MyStruct1 實際的內存大小必須是 8 的整數倍,18向上取整到24,主要是因為24是8的整數倍,所以 sizeof(MyStruct1) 的結果是 24

結構體MyStruct2 內存大小計算

根據內存對齊規則計算MyStruct2的內存大小,詳解過程如下:

  • 變量b:占8個字節,從0開始,此時min(0,8),即 0-7 存儲 b
  • 變量c:占4個字節,從8開始,此時min(8,4),8可以整除4,即 8-11 存儲 c
  • 變量d:占2個字節,從12開始,此時min(12, 2),12可以整除2,即12-13 存儲 d
  • 變量a:占1個字節,從14開始,此時min(14,1),即 14 存儲 a

因此MyStruct2的需要的內存大小為 15字節,而MyStruct1中最大變量的字節數為8,所以 MyStruct2 實際的內存大小必須是 8 的整數倍,15向上取整到16,主要是因為16是8的整數倍,所以 sizeof(MyStruct2) 的結果是 16

結構體嵌套結構體

上面的兩個結構體只是簡單的定義數據成員,下面來一個比較復雜的,結構體中嵌套結構體的內存大小計算情況

  • 首先定義一個結構體MyStruct3,在MyStruct3中嵌套MyStruct2,如下所示
//1、結構體嵌套結構體
struct Mystruct3{
    double b;   //8字節
    int c;      //4字節
    short d;    //2字節
    char a;     //1字節
    struct Mystruct2 str; 
}Mystruct3;

//2、打印 Mystruct3 的內存大小
NSLog(@"Mystruct3內存大小:%lu", sizeof(Mystruct3));
NSLog(@"Mystruct3中結構體成員內存大小:%lu", sizeof(Mystruct3.str));

打印 的結果如下所示


image
  • 分析Mystruct3的內存計算
    根據內存對齊規則,來一步一步分析Mystruct3內存大小的計算過程

    • 變量b:占8個字節,從0開始,此時min(0,8),即 0-7 存儲 b
    • 變量c:占4個字節,從8開始,此時min(8,4),8可以整除4,即 8-11 存儲 c
    • 變量d:占2個字節,從12開始,此時min(12, 2),20可以整除2,即12-13 存儲 d
    • 變量a:占1個字節,從14開始,此時min(14,1),即 14 存儲 a
    • 結構體成員str:str是一個結構體,根據內存對齊原則二結構體成員要從其內部最大成員大小的整數倍開始存儲,而MyStruct2最大的成員大小為8,所以str要從8的整數倍開始,當前是從15開始,所以不符合要求,需要往后移動到16,16是8的整數倍,符合內存對齊原則,所以 16-31 存儲 str

因此MyStruct3的需要的內存大小為 32字節,而MyStruct3中最大變量為str, 其最大成員內存字節數為8,根據內存對齊原則,所以 MyStruct3 實際的內存大小必須是 8 的整數倍,32正好是8的整數倍,所以 sizeof(MyStruct3) 的結果是 32

其內存存儲情況如下圖所示


結構體嵌套結構體的內存存儲情況

二次驗證

為了保險起見,我們再定義一個結構體,來驗證我們結構體嵌套的內存大小計算說明

struct Mystruct4{
    int a;              //4字節 min(0,4)--- (0,1,2,3)
    struct Mystruct5{   //從4開始,存儲開始位置必須是最大的整數倍(最大成員為8),min(4,8)不符合 4,5,6,7,8 -- min(8,8)滿足,從8開始存儲
        double b;       //8字節 min(8,8)  --- (8,9,10,11,12,13,14,15)
        short c;         //1字節,從16開始,min(16,1) -- (16,17)
    }Mystruct5;
}Mystruct4;

分析如下

  • 變量a:占4字節,從0開始,min(0,4),即 0-3存儲a
  • 結構體Mystruct5:從4開始,根據內存對齊原則二,即存儲開始位置必須是最大的整數倍(最大成員為8),min(4,8)不能整除,繼續往后移動,直到8, min(8,8)滿足,從8開始存儲結構體Mystruct5的變量
    • 變量b:占8字節,從8開始,min(8,8),可以整除,即 8-15存儲b
    • 變量c:占2字節,從16開始,min(16,2),可以整除,即16-17存儲c

因此Mystruct4中需要的內存大小是 18字節,根據內存對其原則二,Mystruct4實際的內存大小必須是Mystruct5最大成員b的整數倍,即必須是8的整數倍,所以sizeof(Mystruct4) 的結果是 24

以下是運行結果的打印,以此來印證24這個內存大小

內存優化(屬性重排)

MyStruct1 通過內存字節對齊原則,增加了9個字節,而MyStruct2通過內存字節對齊原則,通過4+2+1的組合,只需要補齊一個字節即可滿足字節對齊規則,這里得出一個結論結構體內存大小與結構體成員內存大小的順序有關

  • 如果是結構體中數據成員是根據內存從小到大的順序定義的,根據內存對齊規則來計算結構體內存大小,需要增加有較大的內存padding即內存占位符,才能滿足內存對齊規則,比較浪費內存

  • 如果是結構體中數據成員是根據內存從大到小的順序定義的,根據內存對齊規則來計算結構體內存大小,我們只需要補齊少量內存padding即可滿足堆存對齊規則,這種方式就是蘋果中采用的,利用空間換時間,將類中的屬性進行重排,來達到優化內存的目的

以下面這個例子來進行說明 蘋果中屬性重排,即內存優化

  • 定義一個自定義CJLPerson類,并定義幾個屬性,
@interface CJLPerson : NSObject

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

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

@implementation CJLPerson

@end
  • 在main中創建CJLPerson的實例對象,并對其中的幾個屬性賦值
int main(int argc, char * argv[]) {
    @autoreleasepool {
        CJLPerson *person = [CJLPerson alloc];
        person.name      = @"CJL";
        person.nickName  = @"C";
        person.age       = 18;
        person.c1        = 'a';
        person.c2        = 'b';

        NSLog(@"%@",person);
    }
    return 0;
}
  • 斷點調試person,根據CJLPerson對象地址,查找出屬性的值
    • 通過地址找出 name & nickName

      image

    • 當我們向通過0x0000001200006261地址找出age等數據時,發現是亂碼,這里無法找出值的原因是蘋果中針對age、c1、c2屬性的內存進行了重排,因為age類型占4個字節,c1和c2類型char分別占1個字節,通過4+1+1的方式,按照8字節對齊,不足補齊的方式存儲在同一塊內存中,

      • age的讀取通過0x00000012
      • c1的讀取通過0x61(a的ASCII碼是97)
      • c2的讀取通過0x62(b的ASCII碼是98)
        image

下圖是CJLPerson的內存分布情況


CJLPerson內存存儲情況

注意:
1、char類型的數據讀取出來是以ASCII碼的形式顯示
2、圖片中地址為0x0000000000000000,表示person中還有屬性未賦值

總結

所以,這里可以總結下蘋果中的內存對齊思想:

  • 大部分的內存都是通過固定的內存塊進行讀取,
  • 盡管我們在內存中采用了內存對齊的方式,但并不是所有的內存都可以進行浪費的,蘋果會自動對屬性進行重排,以此來優化內存

字節對齊到底采用多少字節對齊?

到目前為止,我們在前文既提到了8字節對齊,也提及了16字節對齊,那我們到底采用哪種字節對齊呢?

我們可以通過objc4中class_getInstanceSize的源碼來進行分析

/** 
 * 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) {
    //x+7 & (~7) --> 8字節對齊
    return (x + WORD_MASK) & ~WORD_MASK;
}


//其中 WORD_MASK 為
#   define WORD_MASK 7UL

通過源碼可知:

  • 對于一個對象來說,其真正的對齊方式8字節對齊,8字節對齊已經足夠滿足對象的需求了
  • apple系統為了防止一切的容錯,采用的是16字節對齊的內存,主要是因為采用8字節對齊時,兩個對象的內存會緊挨著,顯得比較緊湊,而16字節比較寬松,利于蘋果以后的擴展。

總結
綜合前文提及的獲取內存大小的方式

  • class_getInstanceSize:是采用8字節對齊,參照的對象的屬性內存大小
  • malloc_size:采用16字節對齊,參照的整個對象的內存大小,對象實際分配的內存大小必須是16的整數倍

內存對齊算法

目前已知的16字節內存對齊算法有兩種

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

align16: 16字節對齊算法

這個算法的思想已經在iOS-底層原理 02:alloc & init & new 源碼分析中有所提及

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

segregated_size_to_fit: 16字節對齊算法

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

算法原理:k + 15 >> 4 << 4 ,其中 右移4 + 左移4相當于將后4位抹零,跟 k/16 * 16一樣 ,是16字節對齊算法,小于16就成0了

以 k = 2為例,如下圖所示


libmalloc中16字節對齊算法原理
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374