OC底層原理六: 內存對齊

OC底層原理 學習大綱

前期準備

1.lldb打印規則

po: 對象信息

(lldb) po person
<HTPerson: 0x101875c70>

p: 對象信息

(lldb) p person
(HTPerson *) $1 = 0x0000000101875c70

xmemory read的簡寫,讀取內存信息 (iOS是小端模式,內存讀取要反著讀)
例如: e5 22 00 00 01 80 1d 00 應讀取為0x001d8001000022e5

(lldb) memory read person
0x1024aef20: c9 21 00 00 01 80 1d 00 00 00 00 00 00 00 00 00  .!..............
0x1024aef30: 2d 5b 4e 53 56 69 73 75 61 6c 54 61 62 50 69 63  -[NSVisualTabPic
(lldb) x person
0x1024aef20: c9 21 00 00 01 80 1d 00 00 00 00 00 00 00 00 00  .!..............
0x1024aef30: 2d 5b 4e 53 56 69 73 75 61 6c 54 61 62 50 69 63  -[NSVisualTabPic

x/4gx: 打印4條16進制的16字符長度的內存信息

(lldb) x/4gx person
0x101875c70: 0x001d8001000022e5 0x0000000000000012
0x101875c80: 0x0000000100001010 0x0000000100001030

x/4gw: 打印4條16進制的8字符長度的內存信息

(lldb) x/4gw person
0x1024aef20: 0x000021c9 0x001d8001 0x00000000 0x00000000

p/t: 二進制打印

(lldb) p/t person
(HTPerson *) $2 = 0b0000000000000000000000000000000100000010010010101110111100100000
2.獲取內存大小
  • sizeof:

    操作符。傳入數據類型,輸出內存大小。編譯時固定
    只與類型相關,與具體數值無關。(如:bool 2字節,int 4字節,對象(指針)8字節)

  • class_getInstanceSize:

    runtime的api,傳入對象,輸出對象所占的內存大小,本質是對象中成員變量的大小

  • malloc_size:

    獲取系統實際分配的內存大小,符合前面章節align16對齊標準

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // insert code here...
        NSObject * objc = [[NSObject alloc] init];
        NSLog(@"[sizeof]                 內存大小: %lu字節", sizeof(objc));
        NSLog(@"[class_getInstanceSize]  內存大小: %lu字節", class_getInstanceSize([objc class]));
        NSLog(@"[malloc_size]            內存大小: %lu字節", malloc_size((__bridge const void *)(objc)));
    }
    return 0;
}
image.png
  • 今天我們就來了解,對象內部內存對齊

內存對齊

我們知道對象對外,蘋果系統會采用align16字節對齊開辟內存大小,提高系統存取性能。

對象內部呢?

  • 對象的本質是結構體,這個在后續篇章中我們會詳細了解。所以研究對象內部的內存,就是研究結構體內存布局

  • 內存對齊目的:最大程度提高資源利用率

我們從一個小案例開始入手

struct MyStruct1 {
    char a;       // 1字節
    double b;     // 8字節
    int c;        // 4字節
    short d;      // 2字節
    NSString * e; // 8字節(指針)
} MyStruct1;

struct MyStruct2 {
    NSString * a; // 8字節(指針)
    double b;     // 8字節
    int c;        // 4字節
    short d;      // 2字節
    char e;       // 1字節
} MyStruct2;

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        NSLog(@"%lu - %lu", sizeof(MyStruct1), sizeof(MyStruct2));
    }
    return 0;
}

打印結果:

image.png
MyStruct1 和 MyStruct2 的構成元素都一樣,為何打印出的內存大小不一致?
  • 結構體內部的元素排序影響內存大小。這就是內存字節對齊的作用。
結構體內存對齊規則

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

注意: 這里的8字節對齊是結構體內部對齊規則,對象在系統中對外實際分配的空間是遵循16字節對齊原則。

【三條結構體對齊規則】:
(先把規則寫出來,我們下面用實例來理解)

  1. 數據成員的對齊規則可以理解為min(m, n) 的公式, 其中 m表示當前成員的開始位置, n表示當前成員所需位數。如果滿足條件 m 整除 n (即 m % n == 0), n 從m 位置開始存儲, 反之繼續檢查 m+1 能否整除 n, 直到可以整除, 從而就確定了當前成員的開始位置

  2. 數據成員為結構體:當結構體嵌套結構體時,作為數據成員的結構體的自身長度作為外部結構體的最大成員的內存大小,比如結構體a嵌套結構體b,b中有char、int、double等,則b的自身長度為8

  3. 最后結構體的內存大小必須是結構體中最大成員內存大小的整數倍,不足的需要補齊。

iOS 基礎數據類型 字節數表

基礎數據類型字節數

MyStruct1 內存計算
MyStruct2 內存計算

結構體中的結構體

struct MyStruct3 {
    NSString * a; // 8字節(指針)
    double b;     // 8字節
    int c;        // 4字節
    short d;      // 2字節
    char e;       // 1字節
    struct MyStruct2 str;
} MyStruct3;

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        NSLog(@"MyStruct3內存大小: %lu", sizeof(MyStruct3));
        NSLog(@"MyStruct3中的結構體(MyStruct2)內存大小 %lu", sizeof(MyStruct2));
    }
    return 0;
}
image.png
MyStruct3 內存計算

內存優化(屬性重排)

  • 我們觀察到MyStruct1MyStruct2的成員屬性一樣,但是在內存管理上,MyStruct2MyStruct1利用率更高(白色空白區域更少)。

  • MyStruct2intshortchar 4 + 2 + 1組合,空間利用得更合理。

  • 蘋果會進行屬性重排,對屬性進行合理排序,盡可能保持保持屬性之間的內存連續,減少padding(白色部分,屬性之間置空的內存)。

如果你還記得align16對齊方式,你應該能理解屬性重排的好處了

  • align16, 是空間換取時間,保障系統在處理對象時能快速存取
  • 屬性重排,保障一個對象盡可能少的占用內存資源。

屬性重排案例

  • 創建HTPerson
@interface HTPerson : NSObject

@property(nonatomic, copy)   NSString * name;
@property(nonatomic, copy)   NSString * nickname;
@property(nonatomic, assign) int        age;
@property(nonatomic, assign) long       height;
@property(nonatomic, assign) char       c1;
@property(nonatomic, assign) char       c2;

@end
  • main.m 加入測試代碼
#import "HTPerson.h"

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        HTPerson * person = [[HTPerson alloc]init];
        person.age      = 18;
        person.height   = 190;
        person.name     = @"mark";
        person.nickname = @"哈哈";
        person.c1       = 'A';
        person.c2       = 'B';
        
        NSLog(@"%@", person);
    }
    return 0;
}
  • x/8gx person: 16進制打印8行內存信息

    image.png

  • 我們分析屬性,namenicknameheight都是各自占用8字節。可以直接打印出來。

  • age是Int占用4字節,c1c2是char,各自占用1字節。我們推測系統可能屬性重排,將他們存放在了一個塊區。

image.png

特殊的doublefloat

我們嘗試把height屬性類型修改為double

@property(nonatomic, assign) double     height;

image.png

我們發現直接po打印0x4067c00000000000,打印不出來height的數值190。 這是因為編譯器po打印默認當做int類型處理。

  • p/x (double)190:我們以16進制打印double類型值打印,發現完全相同。

如果height熟悉換成float,也是一樣的使用p/x (float)190驗證。

我們可以封裝2個驗證函數:

// float轉換為16進制
void ht_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");
}

// float轉換為16進制
void ht_double2HEX(float f){
    union uuf { float f; char s[8];} uf;
    uf.f = f;
    printf("0x");
    for (int i = 7; i>=0; i--) {
        printf("%02x", 0xff & uf.s[i]);
    }
    printf("\n");
}
image.png

為什么對象內部字節對齊是8字節

我們在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);

objc-class.mm可以找到:

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

進入alignedInstanceSize:

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

進入word_align

#ifdef __LP64__ // 64位操作系統
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL  // 7字節遮罩
#   define WORD_BITS 64 
#else 
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

static inline uint32_t word_align(uint32_t x) {
    // (x + 7) & (~7)  --> 8字節對齊
    return (x + WORD_MASK) & ~WORD_MASK;
}

可以看到,系統內部設定64位操作系統,統一使用8字節對齊

總結

  • 外部處理,系統面對的對象太多,我們統一按照align16內存塊來存取,效率很快。(所以malloc_size讀取的都是16的倍數)

  • 但為了避免浪費太多內存空間。系統會在每個對象內部進行屬性重排,并使用8字節對齊,使單個對象占用的資源盡可能小。(所以class_getInstanceSize讀取的都是8的倍數)

  • 外部使用16字節對齊,給類留足夠間距,避免越界訪問,對象內部使用8字節對齊完全足夠。

至此, OC底層原理三:探索alloc (你好,alloc大佬 )中提到的三大核心方法,我們已掌握了initstanceSize計算內存大小。

_class_createInstanceFromZone核心方法.png

下一節: ` OC底層原理七: malloc源碼分析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。