iOS底層原理(一):OC物件實際佔用記憶體與開闢記憶體關係

D_猿員發表於2018-10-29

Objective-C程式語言是C語言的超集,在C語言的基礎上加入了物件導向的內容。OC可以和C/C++混合使用,OC物件都可以轉化為C/C++結構體表示。

要想知道一個NSObject物件佔用多少記憶體,可以通過檢視NSObject物件對應的C++結構體的大小來判斷。

我們可以使用xcode的命令列工具來把指定的OC檔案轉成C++檔案。

//main.m
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
    }
    return 0;
}

clang -rewrite-objc main.m -o main.cpp
// 更加優秀的編譯是根據平臺 和架構來

/**
xcrun                      : xcode  run
 -sdk  iphoneos      : iphoneos作業系統
 -arch  arm64         : arm64架構
*/
xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc  OC原始檔  -o  輸出的CPP檔案 

在生成的main.cpp檔案中可以查詢到NSObject_IMP的程式碼片段,這個就是NSOject物件對應的C++結構體:

struct NSObject_IMPL {
    Class isa; //一個指向struct objc_class結構體型別的指標
};

// 檢視Class本質
typedef struct objc_class *Class;

我們通過NSObject物件對應的結構體發現,結構體中只有一個isa指標變數。按理來說NSObject物件需要的記憶體大小隻要能夠滿足存放一個指標大小就可以了,一個指標變數在64位的機器上大小是8個位元組(我們只討論64位的機器大小),也就是說只要有8個位元組的記憶體空間就能滿足存放一個NSObject物件了。

那是不是說一個NSObject物件就佔用8個位元組大小的記憶體呢?實際上不是這樣的。我們需要分清楚兩個概念,物件佔用的記憶體空間和物件實際利用的記憶體空間。我們可以用坐車的例子來說明一下這兩個概念的區別:物件佔用的記憶體空間就好比汽車的載客數量,物件實際利用的記憶體空間就好比車上實際的乘客數量,實際的乘客數量是不會超過車輛的最大載客數量的,也不會存在空載的情況。NSObject物件中的isa指標變數就好比車上的乘客,實際上系統給一個NSObject物件分配了16個位元組大小的記憶體空間。實際情況我們可以通過下面的程式碼來驗證一下:

#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物件實際需要的記憶體大小: %zd", class_getInstanceSize([objc class]));
        NSLog(@"objc物件實際分配的記憶體大小: %zd", malloc_size((__bridge const void *)(objc)));
    }
    return 0;
}

//輸出的結果
2018-08-02 23:31:39.891056+0800 OC物件的本質[8461:5063635] objc物件實際利用的記憶體大小: 8
2018-08-02 23:31:39.891320+0800 OC物件的本質[8461:5063635] objc物件實際佔用的記憶體大小: 16

一個物件實際利用的記憶體大小,就是物件的例項變數佔用的記憶體大小,可以通過呼叫runtime中的class_getInstanceSize函式得到。物件實際佔用的記憶體大小,就是系統實際分配給物件的記憶體大小,OC物件是通過alloc方法得到的物件大小,我們可以通過malloc中庫函式malloc_size來得到結果。

為什麼一個NSObject物件明明只需要8個位元組的記憶體大小就可以了,但是還是分配到了16個位元組大小的記憶體空間?對於這個問題我們可以通過閱讀objc4的原始碼來找到答案。通過檢視跟蹤obj4中alloc和allocWithZone兩個函式的實現,會發現這個連個函式都會呼叫一個instanceSize的函式:

size_t instanceSize(size_t extraBytes) {
     size_t size = alignedInstanceSize() + extraBytes;
      // CF requires all objects be at least 16bytes.
      if (size < 16) size = 16;
      return size; 
}

這個函式的程式碼很簡單,返回的結果就是系統給一個物件分配記憶體的大小。當物件的實際大小小於16時,系統就返回16個位元組的大小。也就是說16個位元組大小是系統的最低消費。還是用坐車的例子來說明一下,假如有8個人想坐車,他們打電話叫車說要一輛能坐8個人大小的車,對方說sorry我們沒有坐8個人大小的車,我們這裡最小的就是坐16個人的車。最後來了一輛坐16個人的車,拉了8個人開走了。車就好比一個NSOject物件,車上的乘客就好比是物件中的成員,車的大小或者說載客數量就相當於一個物件佔用的記憶體大小,車上實際的乘客數量就是物件中成員的大小。所以說一個NSObject物件佔用多少記憶體,我想應該很明白了。

我們可以繼續深入一點,推算針對我們自定義的類記憶體佈局和物件佔用的記憶體空間。假設我們定義一個Animal的類,其中只有一個int成員變數weight。

//interface
@interface Animal: NSObject
{
    int weight;
}
@end
//implementation
@implementation Animal
@end

我們同樣可以通過把OC檔案轉化為C++檔案的方式來檢視Animal類對應的結構體實現。

struct Animal_IMPL {
    struct NSObject_IMPL NSObject_IVARS; //實際上就是一個isa指標
    int weight;
};

struct NSObject_IMPL {
    Class isa; //一個指向struct objc_class結構體型別的指標
};

//簡化版本
struct Animal_IMPL {
    Class isa;
    int weight;
};

通過struct Animal_IMPL結構體,我們不難看出結構體中有兩個成員變數:一個isa指標和一個int型成員變數。Animal結構體物件實際需要的記憶體大小應該是16位元組(指標8個位元組,int型變數4個位元組)。Animal結構體物件實際需要的記憶體大小是12位元組,那系統給Animal物件實際分配的記憶體大小是多少呢?我們還是通過呼叫class_getInstanceSize和malloc_size這兩個函式來看一下。

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

//interface
@interface Animal: NSObject
{
    int weight;
}
@end

//implementation
@implementation Animal
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
        Animal *animal = [[Animal alloc] init];
        NSLog(@"animal物件實際需要的記憶體大小: %zd", class_getInstanceSize([animal class]));
        NSLog(@"animal物件實際分配的記憶體大小: %zd", malloc_size((__bridge const void *)(animal)));
    }
    return 0;
}

//輸出結果
2018-08-03 13:56:38.463885+0800 OC物件的本質[7461:967943] animal物件實際利用的記憶體大小: 16
2018-08-03 13:56:38.463899+0800 OC物件的本質[7461:967943] animal物件實際佔用的記憶體大小: 16

我們發現Animal物件實際需要的記憶體大小是16位元組,而不是我們之前推算出來的12位元組,這其中涉及到了結構體成員變數的記憶體對齊的問題,結構體記憶體對齊其中有一條要求結構體大小需要是最大成員變數大小的整數倍,這裡的最大成員變數是指標變數(8個位元組),結構體的最終的大小需要是8的整數倍,所以結果是16而不是12。系統實際分配的大小也是16位元組,這個就比較好理解了,之前我們提到系統最小分配的記憶體大小是16位元組。

我們可以在Animal類中增加一個int成員變數,此時新的物件實際需要的記憶體和實際分配得到的記憶體大小是多少呢?答案是都是16個位元組大小。新增了一個4位元組大小int型的變數,實際需要的記憶體大小就是8+4+4=16位元組,系統實際分配的大小也是16位元組。

如果我們再增加一個int型的成員變數的話,物件實際需要的記憶體和實際分配得到的記憶體大小是多少呢?我們可以簡單的推算一下,物件結構中有4個成員變數,一個指標變數和3個int型變數,4個成員變數的記憶體大小加起來是20(8+4+4+4)個位元組大小,根據結構體記憶體對齊的原則,結構體實際需要的記憶體大小應該是8的整數倍,也就是24個位元組。物件實際需要的記憶體大小是24個位元組,那麼系統實際分配給物件的記憶體大小又是多少呢?我們可以通過程式碼來檢視一下最終的結果。

@interface Animal: NSObject
{
    int weight;
    int height;
    int age;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {      
        Animal *animal = [[Animal alloc] init];
        NSLog(@"animal物件實際需要的記憶體大小: %zd", class_getInstanceSize([animal class]));
        NSLog(@"animal物件實際分配的記憶體大小: %zd", malloc_size((__bridge const void *)(animal)));
    }
    return 0;
}

//輸出結果
2018-08-03 17:45:59.468699+0800 OC物件的本質[11044:1911186] animal物件實際需要的記憶體大小: 24
2018-08-03 17:45:59.468719+0800 OC物件的本質[11044:1911186] animal物件實際分配的記憶體大小: 32

新的Animal物件實際需要的記憶體大小是24位元組,但是系統給物件實際分配的記憶體大小是32位元組。這有時為什麼呢?我們需要檢視相關的資料和Apple的關於malloc的開原始碼才能弄清楚其中的原因。具體原因是Apple系統中的malloc函式分配記憶體空間時,記憶體是根據一個bucket的大小來分配的. bucket的大小是16,32,48,64,80 …,可以看出系統是按16的倍數來分配物件的記憶體大小的。

我們可以再增加兩個double型的成員變數來進一步的做驗證。

@interface Animal: NSObject
{
    int weight;
    int height;
    int age;
    double d1;
    double d2;
}
@end

我們能夠在不執行程式碼的情況下推算出物件實際需要和系統實際分配的記憶體大小。物件的成員變數的記憶體大小是36(8+4+4+4+8+8)個位元組,但是需要記憶體對齊,最終物件實際需要的記憶體是40位元組。系統分配的記憶體大小是48位元組。

通過把OC物件轉化為C++結構體的方法,我們很容易搞清楚OC物件的記憶體分配情況

以上就是這篇文章的全部內容了,希望本文的內容對大傢俱有一定的參考學習價值,如果有疑問大家可以進入小編交流群:624212887,一起交流學習,謝謝大家的支援

相關文章