1. 程式人生 > Android開發 >OC 底層 物件的本質

OC 底層 物件的本質

前言

探尋OC物件的本質,我們平時編寫的Objective-C程式碼,底層實現其實都是C\C++程式碼。

OC的物件都是通過基礎C\C++的結構體實現的。

OC 轉換為 C++

我們通過建立OC物件,並將OC檔案轉化為C++檔案來探尋OC物件的本質,如下程式碼:

#import <Foundation/Foundation.h>
int main(int argc,const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
    }
    return
0; } 複製程式碼

使用 clang 將 OC 程式碼轉換為 C++ 程式碼

// 這種方式沒有指定架構例如arm64架構 其中cpp代表(c plus plus)生成 main.cpp
clang -rewrite-objc main.m -o main.cpp 
複製程式碼

使用 Xcode 工具 xcrun 進行轉換

// 可以指定 arm64 架構 如果需要連結其他框架,使用-framework引數。比如-framework UIKit
// -o 輸出的 cpp 檔案
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 
複製程式碼

NSObject 物件的記憶體佈局

NSObject 物件的 OC 定義如下:(我們可以在Xcode中點選進去檢視)

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}


複製程式碼

NSObject 對應的結構體是 NSObject_IMPL結構體,NSObject_IMPL 定義如下:

struct NSObject_IMPL {
    Class isa;
};
typedef struct objc_class *Class;

複製程式碼

NSObject_IMPL結構體只有一個成員 isa

指標,而指標在64位架構中佔8個位元組。也就是說一個NSObjec物件所佔用的記憶體是8個位元組。

NSObject *objc = [[NSObject alloc] init];
複製程式碼

上述一段程式碼中系統為 NSObject 物件分配 8個位元組的記憶體空間,用來存放一個成員 isa 指標。那麼 isa 指標這個變數的地址就是結構體的地址,也就是 NSObjcet 物件的地址。

NSObject 只需要8位元組的空間,但實際上,NSObject 物件佔用 16 位元組空間,iOS 下物件至少佔用 16 位元組

NSObject 子類物件的記憶體佈局

程式碼如下:

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

int main(int argc,const char * argv[]) {
    @autoreleasepool {
        Student *stu = [[Student alloc] init];
        stu -> _no = 4;
        stu -> _age = 5;
        
        NSLog(@"%@",stu);
    }
    return 0;
}
@end

複製程式碼

對應的 C++ 結構體 Student_IMPL 如下:

struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _no;
    int _age;
};
複製程式碼

因此此結構體佔用多少儲存空間,物件就佔用多少儲存空間。因此結構體佔用的儲存空間為,isa指標8個位元組空間,int型別 _no 佔用4個位元組空間,int型別 _age 佔用4個位元組空間,共16個位元組空間。

image.png

sutdent物件的3個變數分別有自己的地址。而stu指向isa指標的地址。因此stu的地址為0x100400110,stu物件在記憶體中佔用16個位元組的空間。並且經過賦值,_no裡面儲存4 ,_age裡面儲存5。

驗證物件的記憶體佈局

struct Student_IMPL {
    Class isa;
    int _no;
    int _age;
};
@interface Student : NSObject
{
    @public
    int _no;
    int _age;
}
@end

@implementation Student
int main(int argc,const char * argv[]) {
    @autoreleasepool {
            // 強制轉化
            Student *stu = [[Student alloc] init];
            stu -> _no = 4;
            stu -> _age = 5;
            struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
            NSLog(@"_no = %d,_age = %d",stuImpl->_no,stuImpl->_age); // 打印出 _no = 4,_age = 5
    }
    return 0;
}
@end
複製程式碼

上述程式碼將oc物件強轉成Student_IMPL的結構體。也就是說把一個指向oc物件的指標,指向這種結構體。最終可以轉化成功,所以物件在記憶體中的佈局與結構體在記憶體中的佈局相同。由此說明stu這個物件指向的記憶體確實是一個結構體。

更復雜的繼承關係

///  Person 
@interface Person : NSObject
{
    int _age;
}
@end
/// Student
@interface Student : Person
{
    int _no;
}
@end
int main(int argc,const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"%zd  %zd",class_getInstanceSize([Person class]),class_getInstanceSize([Student class])
              );
    }
    return 0;
}
複製程式碼

我們發現只要是繼承自NSObject的物件,那麼底層結構體內一定有一個isa指標。那麼他們所佔的記憶體空間是多少呢?單純的將指標和成員變數所佔的記憶體相加即可嗎?上述程式碼實際列印的內容是16 16,也就是說,person物件和student物件所佔用的記憶體空間都為16個位元組。 其實實際上person物件確實只使用了12個位元組。但是因為記憶體對齊的原因。使person物件也佔用16個位元組。

編譯器在給結構體開闢空間時,首先找到結構體中最寬的基本資料型別,然後尋找記憶體地址能是該基本資料型別的整倍的位置,作為結構體的首地址。將這個最寬的基本資料型別的大小作為對齊模數。
為結構體的一個成員開闢空間之前,編譯器首先檢查預開闢空間的首地址相對於結構體首地址的偏移是否是本成員的整數倍,若是,則存放本成員,反之,則在本成員和上一個成員之間填充一定的位元組,以達到整數倍的要求,也就是將預開闢空間的首地址後移幾個位元組。
複製程式碼

我們可以總結記憶體對齊為兩個原則:

原則 1. 前面的地址必須是後面的地址正數倍,不是就補齊。
原則 2. 整個Struct的地址必須是最大位元組的整數倍。
原則 3. OC 物件至少佔 16 位元組
複製程式碼

通過上述記憶體對齊的原則我們來看,person物件的第一個地址要存放isa指標需要8個位元組,第二個地址要存放_age成員變數需要4個位元組,根據原則一,8是4的整數倍,符合原則一,不需要補齊。然後檢查原則2,目前person物件共佔據12個位元組的記憶體,不是最大位元組數8個位元組的整數倍,所以需要補齊4個位元組,因此person物件就佔用16個位元組空間。

而對於student物件,我們知道sutdent物件中,包含person物件的結構體實現,和一個int型別的_no成員變數,同樣isa指標8個位元組,_age成員變數4個位元組,_no成員變數4個位元組,剛好滿足原則1和原則2,所以student物件佔據的記憶體空間也是16個位元組。

程式碼方式獲取物件佔用空間大小

建立一個例項物件,至少需要多少記憶體?
#import <objc/runtime.h> class_getInstanceSize([NSObject class]);
●
●
●
建立一個例項物件,實際上分配了多少記憶體?
#import <malloc/malloc.h> malloc_size((__bridge const void *)obj);

備註:sizeof()不是一個函式

複製程式碼

由於一些系統記憶體分配機制,記憶體對齊等,實際分配的記憶體可能要比實際需要的記憶體大一些。