NSObject 物件佔用記憶體、isa/superclass指向、類資訊存放

JacobLJ發表於2018-09-09

注:分析步驟參考 MJ底層原理班 內容,本著自己學習原則記錄

本文使用的原始碼為objc4-723

1 一個NSObject物件佔用多少記憶體?

  1. OC底層實現是C/C++,OC 物件的底層表現為 C/C++的結構體
  2. 結構體的大小,實際上是指它內部所有成員變數佔用記憶體的大小 (存在記憶體對齊原則,指的是結構體的記憶體大小必須是最大成員變數記憶體的大小的倍數關係)
  3. 在64bit 下,NSObject類的結構體物件只包含一個 Class 型別指標的 isa 成員變數
  4. 按照第3點的理解,NSObject 物件佔用的記憶體應該就只有8個位元組的空間 (64bit 下,可以通過class_getInstanceSize函式獲得,其內部會進行記憶體對齊操作)
  5. 但實際情況是:系統分配了16個位元組給 NSObject 物件 (通過 malloc_size 函式獲得)

~以下是上述5點的解析:~

1.1 OC 程式碼通過兩種方法獲得的大小

  • 使用 Xcode 建立 macOS 類的 command line 專案,程式碼如下:
#import <Foundation/Foundation.h>

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
        
        // 獲得 NSObject 類例項物件的成員變數所佔用的大小
        NSLog(@"%zd", class_getInstanceSize([NSObject class]));
        
        // 獲得 obj 指標指向記憶體的大小
        NSLog(@"%zd", malloc_size((__bridge const void *)obj));
        
    }
    return 0;
}


>>>> 列印結果
 8
16
複製程式碼

1.2 class_getInstanceSizemalloc_size說明

  • API 說明
extern size_t malloc_size(const void *ptr);
    /* Returns size of given ptr */
複製程式碼
/** 
 * 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);
複製程式碼
  • 兩個函式使用場景
建立一個例項物件,至少需要多少記憶體?
#import <objc/runtime.h>
class_getInstanceSize([NSObject class]); 

等價於 sizeof()獲得的值。
sizeof 獲取型別大小,它是一個運算子並非函式,在編譯時即計算到給定型別的大小,即如 sizeof(int) 在編譯後會直接替換為 4。 
由於在編譯時計算,因此sizeof不能用來返回動態分配的記憶體空間的大小,而class_getInstanceSize則屬於動態獲取
複製程式碼
建立一個例項物件,實際上分配了多少記憶體?
#import <malloc/malloc.h>
malloc_size((__bridge const void *)obj);

複製程式碼

1.3 通過原始碼解析class_getInstanceSize方法返回8個位元組原因

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() {
        return word_align(unalignedInstanceSize());
    }
複製程式碼
  • 函式alignedInstanceSize的描述是 Class's ivar size rounded up to a pointer-size
  • 這個方法是獲取類 ivar(成員變數) 的大小,這就解析為什麼方法class_getInstanceSize返回的是8個位元組了

因為 NSObject 物件中只有一個 isa 指標成員變數,而且 isa 的型別是一個指標。在64bit 裝置下指標大小為8個位元組

1.4 將 OC 轉成 C/C++程式碼, 解析NSObject本質

  • OC 中 NSObject 的定義,只有一個 isa 指標
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
複製程式碼
  • 通過命令列將 OC 的 mian.m 檔案轉化為 C++ 檔案

方式一:簡單轉換

clang -rewrite-objc main.m -o main.cpp // 這種方式沒有指定架構,如 arm64 架構生成 main.cpp
複製程式碼

方式二:使用xcode工具 xcrun,指定架構模式

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 
複製程式碼

在生成的 main-arm64.cpp 檔案中搜尋NSObjcet,可以找到NSObjcet_IMPL(IMPL代表 implementation 實現)

  • C++ 的結構體
struct NSObject_IMPL {
	Class isa;
};
// Class其實就是一個指標,型別如下
// typedef struct objc_class *Class;
複製程式碼

1.5 為什麼有8個位元組又有16個位元組的問題呢?

  • 通過在原始碼中追蹤allocWithZone函式獲得解答 我們知道,建立物件時,NSObject *obj = [[NSObject alloc] init];會呼叫alloc類方法,而其底層就是呼叫allocWithZone
  1. 底層都是呼叫 callAlloc

    搜尋 allocWithZone 獲取對應資訊

  2. class_createInstance方法建立 obj

    class_createInstance

  3. 找到分配記憶體函式instanceSize

    NSObject 物件佔用記憶體、isa/superclass指向、類資訊存放

  4. 原因: corefoundation 要求所有 objects 最少16 bytes

    最少16 bytes

1.6 簡單繼承的物件記憶體佔用分析

  • 一個Person物件、一個Student物件佔用多少記憶體空間?
#import <Foundation/Foundation.h>

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


/* Person */
@interface Person : NSObject {
    int _age;
}
@end

@implementation Person
@end

/* Student */
@interface Student : Person {
    int _no;
}
@end

@implementation Student
@end



int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person = [[Person alloc] init];
        NSLog(@"person - %zd", class_getInstanceSize([Person class]));
        NSLog(@"person - %zd", malloc_size((__bridge const void *)person));
        
        
        Student *stu = [[Student alloc] init];
        NSLog(@"stu - %zd", class_getInstanceSize([Student class]));
        NSLog(@"stu - %zd", malloc_size((__bridge const void *)stu));
        
    }
    return 0;
}

>>>>列印結果
person - 16
person - 16
stu - 16
stu - 16
複製程式碼

1.6.1 轉成 C++ 後結構體成員分析

  • 下述程式碼分析可以通過上述對 NSObject 物件的分析步驟獲得
    摘自 MJ 底層原理班課程 PPT

1.6.2 記憶體對齊

  1. 記憶體對齊:結構體的大小必須是最大成員大小的倍數
  2. Person 中的實際應該分配應該是12個位元組,因為 isa 為8位元組,int 型別的 _age 為4位元組,為什麼class_getInstanceSize返回的還是16位元組呢?此時就要考慮記憶體對齊了。以最大成員大小,即8位元組的 isa 的倍數算,最少就是8的2倍,16位元組了。
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 8
    int _age; // 4
}; // 16 記憶體對齊:結構體的大小必須是最大成員大小的倍數
複製程式碼

1.6.3 優先利用空的連續的記憶體

  1. 上述中 Person 例項實際使用的記憶體是12位元組,但是記憶體佔用是16位元組,那麼多餘的4個位元組在 Student 例項建立時就需要被考慮使用了。
struct Student_IMPL {
    struct Person_IMPL Person_IVARS; // 16
    int _no; // 4
}; // 16,剛好 Person 分配的16位元組中空餘的4個位元組可以放下 int 型別的 _no 成員變數
複製程式碼

1.7 帶@property的物件記憶體佔用分析

// Person
@interface Person : NSObject
{
    int _age;
}
@property (nonatomic, assign) int height;
@end

@implementation Person
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person = [[Person alloc] init];
        NSLog(@"person - %zd", class_getInstanceSize([Person class]));
        NSLog(@"person - %zd", malloc_size((__bridge const void *)person));

    }
    return 0;
}

>>>>列印結果
person - 16
person - 16
複製程式碼
  1. @property 是作用是自動生成一個帶下劃線的例項變數,同時生成對應的getter 和 setter 方法
  2. 那麼轉換成 C++ 後程式碼如下
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 8
    int _age; // 4
    int _height; // 4
};  // 16
複製程式碼
  1. 也就如執行所得,佔用記憶體為16個位元組

1.8 為什麼例項方法不在例項物件裡呢?

  1. 例項方法是公用的,一份足以應付同一型別的多個例項物件。因為除了例項變數的值會變之外,方法的呼叫是不會變的。也就是 person1、person2、person3 它們呼叫 Person 類的方法都是一樣的。

1.9 附加課程介紹的記憶體分析和修改記憶體的操作

  • 分析 stu 例項記憶體 OC 程式碼
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

@interface Student : NSObject {
    @public
    int _no;
    int _age;
}
@end

@implementation Student
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *stu = [[Student alloc] init];
        stu->_no = 4;
        stu->_age = 5;
        
        NSLog(@"%zd", class_getInstanceSize([Student class]));
        NSLog(@"%zd", malloc_size((__bridge const void *)stu));
        
    }
    return 0;
}
複製程式碼

C++ 程式碼

struct NSObject_IMPL {
    Class isa; // 8
};


struct Student_IMPL {
    Class isa;
    int _no;
    int _age;
};

複製程式碼

1.9.1 實時檢視記憶體資料

  1. 在 stu 生成後,打斷點。
  2. 在 Xcode 的控制器檢視 stu 實時地址
  3. 在Xcode 工具欄 選擇 Debug -> Debug Workfllow -> View Memory (Shift + Command + M)然後在 address 中輸入物件的地址
    View Memory

輸入地址

  • 注意,上圖讀取記憶體時,存在大端小端讀取方向問題。

從上圖中,我們可以發現讀取資料從高位資料開始讀,檢視前16位位元組,每四個位元組讀出的資料為 16進位制 0x00 00 00 04(4位元組)、 0x00 00 00 05(4位元組)、 isa的地址為 0x 00 D1 08 10 00 00 11 19(8位元組)

1.9.2 LLDB 指令檢視且修改記憶體值

1.9.2.1 在生成 stu 例項後,打斷點,啟動 LLDB。

1.9.2.2 通過 p 指令獲得 stu 的地址

(lldb) p stu
(Student *) $0 = 0x000000010062cdd0
複製程式碼

1.9.2.3 通過指令memory read讀取對應的地址記憶體

(lldb) memory read 0x000000010062cdd0
0x10062cdd0: c9 11 00 00 01 80 1d 00 04 00 00 00 05 00 00 00  ................
0x10062cde0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
(lldb) 
複製程式碼

指令memory read 可以簡寫成 x

(lldb) x 0x000000010062cdd0
0x10062cdd0: c9 11 00 00 01 80 1d 00 04 00 00 00 05 00 00 00  ................
0x10062cde0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
(lldb) 
複製程式碼

1.9.2.4 增加讀取條件

memory read/[數量][格式][位元組數] 記憶體地址 簡寫: x/[數量][格式][位元組數] 記憶體地址

格式:x是16進位制,f是浮點,d是10進位制 位元組大小:b:byte 1位元組,h:half word 2位元組,w:word 4位元組,g:giant word 8位元組

示例:x/4xw /後面表示如何讀取資料 w:表示4個4個位元組讀取 x:表示以16進位制的方式讀取資料 4:則表示讀取4次

(lldb) memory read/4xw 0x000000010062cdd0
0x10062cdd0: 0x000011c9 0x001d8001 0x00000004 0x00000005
複製程式碼

簡寫

(lldb) x/4xw 0x000000010062cdd0
0x10062cdd0: 0x000011c9 0x001d8001 0x00000004 0x00000005
(lldb) 
複製程式碼

1.9.2.5 修改記憶體中的值

  • 如,修改_no 的值為8則:
// 物件地址 +8個位元組就是 _no 的地址
(lldb) memory write 0x000000010062cdd8 8
複製程式碼

NSObject 物件佔用記憶體、isa/superclass指向、類資訊存放

  • 通過 log 日誌檢視執行步驟
(lldb) p stu
(Student *) $0 = 0x0000000100600590
2018-07-08 11:08:51.663461+0800 Test1 [21943:3939579] no is 4, age is 5
(lldb) memory write 0x0000000100600598 8
2018-07-08 11:09:15.525190+0800 Test1[21943:3939579] -------------
2018-07-08 11:09:15.525299+0800 Test1[21943:3939579] no is 8, age is 5
Program ended with exit code: 0
複製程式碼
  • _no 的值在第一個斷點執行前,通過命令memory write 0x0000000100600598 8進行修改了。值從4 變成 8

1.10 OC物件記憶體分配對齊規則為16的倍數(最大是256)

  • 下述物件中即使實際佔大小為24,但由於記憶體分配對齊原則,最終分配給物件記憶體就是32
#import <Foundation/Foundation.h>
#import <malloc/malloc.h>
#import <objc/runtime.h>

// C++程式碼中物件的結構體表示
//struct NSObject_IMPL
//{
//    Class isa;
//};
//
//struct Person_IMPL
//{
//    struct NSObject_IMPL NSObject_IVARS; // 8
//    int _age; // 4
//    int _height; // 4
//    int _no; // 4
//}; // 24


@interface Person : NSObject {
    int _age;
    int _height;
    int _no;
}
@end
@implementation Person
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        
        NSLog(@"%zd", sizeof(struct Person_IMPL)); // 24
        
        NSLog(@"%zd %zd",
              class_getInstanceSize([Person class]), // 24
              malloc_size((__bridge const void *)(p))); // 32
    }
    return 0;
}
複製程式碼

PS:更復雜的記憶體分配後續補充

2. 物件的isa指標指向哪裡?

問題簡答:

  1. instance物件的isa指標指向class物件
  2. class物件的isa指標指向meta-class物件
  3. meta-class物件的isa指標指向基類的meta-class物件
  4. 基類自己的isa指標也指向自己

問題理解方向如下:

2.1 OC物件的分類

  • 主要可以分為3種 instance物件(例項物件) class物件(類物件) meta-class物件(元類物件)

2.2 instance物件在記憶體中儲存的資訊,主要包括

  • isa指標
  • 其他成員變數(具體的值的資訊等)
  • ...

2.3 class物件在記憶體中儲存的資訊,主要包括

  • isa指標
  • superclass指標
  • 類的成員變數資訊(ivar)(變數名稱之類)
  • 類的屬性資訊(@property)
  • 類的協議資訊(protocol)
  • 類的物件方法資訊(instance method)
  • ...

2.4 meta-class物件和class物件的記憶體結構是一樣的,但是用途不一樣,在記憶體中儲存的資訊,主要包括

  • isa指標
  • superclass指標
  • 類的類方法資訊(class method)
  • ...

2.5 instance、class、meta-class 儲存區別

  • 每一個類通過 alloc 建立的 instance都是獨立佔用一塊記憶體的
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];

obj1 和 obj2 是NSObject的instance物件(例項物件),分別佔用兩塊不同的記憶體
複製程式碼
  • 每個類在記憶體中有且只有一個class物件
Class objClass1 = [obj1 class];
Class objClass2 = [obj2 class];
Class objClass3 = [NSObject class];
Class objClass4 = object_getClass(obj1); //Runtime API
Class objClass5 = object_getClass(obj2); //Runtime API

objClass1~5 都是NSObject 的 class 物件,它們都是同一個物件
複製程式碼
  • 每個類在記憶體中有且只有一個meta-class物件
Class objMetaClass = object_getClass([NSObject class]); // Runtime API
//類物件作為引數獲取元類物件

objMetaClass是NSObject的meta-class物件(元類物件)
複製程式碼
  • 注意: 以下方式獲得的是 class 物件,不是 meta-class物件
    Class objClass = [[NSObject class] class];
    複製程式碼
    檢視 Class 是否為meta-class
    #import <objc/runtime.h>
    BOOL result = class_isMetaClass([NSObject   class]);
    複製程式碼

2.6 isa 和 superclass 指向總結

isa 和 superclass 指向圖,也是例項方法和類方法查詢路線圖

  • 上圖解讀
  1. instance的isa指向class

  2. class的isa指向meta-class

  3. meta-class的isa指向基類的meta-class

  4. class的superclass指向父類的class 如果沒有父類,superclass指標為nil

  5. meta-class的superclass指向父類的meta-class 基類的meta-class的superclass指向基類的class

  6. instance呼叫物件方法的軌跡 isa找到class,方法不存在,就通過superclass找父類

  7. class呼叫類方法的軌跡 isa找meta-class,方法不存在,就通過superclass找父類

2.7 objc_getClass 和 object_getClass

1. objc_getClass

  1. 根據類名字串返回一個類物件
  2. 與 -class、+clas 方法返回結果一樣,都只返回類物件,即使繼續多次呼叫都不會返回 meta-class物件

objc_getClass

2. object_getClass

  1. 如果傳入的是 instance物件 則返回 class物件
  2. 如果傳入的是 class物件 則返回 meta-class物件
  3. 如果傳入是 meta-class物件 則返回 NSObject(基類/rootObject)的 meta-class物件

object_getClass

3. objc_getClass、object_getClass 和 class 小結

NSObject 物件佔用記憶體、isa/superclass指向、類資訊存放

3. OC的類資訊存放在哪裡?

  • instance物件含有資訊為: 1. 成員變數的具體值(ivar value)

  • class物件含有資訊為: 1.物件方法(instance method) 2. 協議(protocol) 3. 屬性(property) 4. 成員變數資訊(ivar type and name etc.info)

  • meta-class物件含有資訊為: 1. 類方法(class method)

    摘自 MJ 底層課課件


文/Jacob_LJ(掘金作者)

PS:如非特別說明,所有文章均為原創作品,著作權歸作者所有,轉載需聯絡作者獲得授權,並註明出處,所有打賞均歸本人所有!

相關文章