iOS底層原理探索—OC對象的本質(zhì)

在探索OC對象的本質(zhì)之前,我們要明白Objective-C的代碼,底層的實現(xiàn)都是C/C++代碼。


OC代碼轉(zhuǎn)化過程.png

而OC中的對象、類則是基于C/C++的結(jié)構(gòu)體來實現(xiàn)的。

我們可以通過將創(chuàng)建好的OC文件,轉(zhuǎn)化成C++文件來看一下OC對象的底層結(jié)構(gòu)。

OC代碼轉(zhuǎn)換成C++

通過命令行將OC的main.m文件轉(zhuǎn)換成C++,生成 main.cpp。

   clang -rewrite-objc main.m -o main.cpp 
/***rewrite代表 重寫
   *-o代表 輸出
   *cpp代表 c++(c plus plus)
**/

需要注意這種方式?jīng)]有指定運行平臺和架構(gòu)模式,我們可以通過命令行設(shè)置參數(shù),來指定運行平臺和架構(gòu)模式

   xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 
/***xcrun代表 xcode
   * iphoneos代表 運行在iPhone上
   *-arch代表 架構(gòu)模式,arm64參數(shù)代表64位的架構(gòu)模式
**/

生成的main-arm64.cpp 文件就是main.m轉(zhuǎn)換c++后的文件,直接拖拽到工程中,就可以查看底層實現(xiàn)了。我們在main-arm64.cpp 文件中搜索NSObjcet,可以找到NSObjcet_IMPL(IMPL即 implementation 的縮寫,代表實現(xiàn))。

struct NSObject_IMPL {
    Class isa;
};

可以看到,NObject的底層實現(xiàn)就是一個結(jié)構(gòu)體。我們進一步查看Class

typedef struct objc_class *Class;

我們發(fā)現(xiàn)Class其實就是一個指針,指向了objc_class類型的結(jié)構(gòu)體。

OC對象內(nèi)存空間

通過以上探索我們可以得出總結(jié),OC中對象的底層實現(xiàn)都是通過C\C++的結(jié)構(gòu)體實現(xiàn)的。那么一個OC對象占據(jù)了多少內(nèi)存空間呢?下面讓我們用代碼實際驗證一下。

NSObject對象占多少內(nèi)存空間.png

通過代碼我們創(chuàng)建了一個NSObject對象,分別調(diào)用了class_getInstanceSizemalloc_size方法來查看占據(jù)的內(nèi)存空間。我們看到打印輸出了8和16,這是怎么回事呢?
我們通過查看OC源碼可知,class_getInstanceSize返回類的成員變量占據(jù)內(nèi)存的大小。而malloc_size獲取obj指針指向內(nèi)存的大小
我們上文創(chuàng)建的NSObject對象obj的結(jié)構(gòu)體只有一個成員:isa指針,而指針在64位架構(gòu)中占8個字節(jié),所以第一次打印輸出了8。但是系統(tǒng)為obj對象分配了16個字節(jié)的內(nèi)存,第二次打印輸出16。
NSObject對象占據(jù)內(nèi)存空間示意圖.jpeg

上圖中黃色區(qū)域系統(tǒng)為obj對象分配的16個字節(jié)的內(nèi)存,但是obj對象結(jié)構(gòu)體中只有一個isa指針,所以只占據(jù)了綠色區(qū)域的8個字節(jié)。這一點可以通過查看alloc底層源碼也可以證實:
alloc源碼.jpeg

通過alloc底層源碼可以看到,如果一個對象占據(jù)的內(nèi)存小于16時,則會直接返回16,也就是說,系統(tǒng)在為對象分配內(nèi)存空間時,最小為16個字節(jié)。

至此我們知道了系統(tǒng)在創(chuàng)建一個對象的時候會為其分配最少16字節(jié)的內(nèi)存空間,其中對象的isa指針占據(jù)8個字節(jié)。我們通過下面一道面試題繼續(xù)深入了解OC對象的底層本質(zhì)。

在64位環(huán)境下,下面的代碼會輸出什么?

/* 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 {
        
        NSLog(@"%zd  %zd",
              class_getInstanceSize([Person class]),
              class_getInstanceSize([Student class])
              );
    }
    return 0;
}

經(jīng)過代碼分析,這道面試題想問的其實就是一個Person對象和一個Student對象分別占據(jù)多少內(nèi)存空間。但是需要注意的是Person對象和Student對象分別有一個實例變量、Student繼承自Person對象。

經(jīng)過綜合上文簡單分析,我們可以簡單畫出Person對象和Student對象的內(nèi)存結(jié)構(gòu)示意圖:

Person對象和Student對象內(nèi)存結(jié)構(gòu)示意圖.png

我們可以看到,在Person對象結(jié)構(gòu)體中,有一個父類類型的結(jié)構(gòu)體指針和一個int類型成員變量_age,分別占據(jù)內(nèi)存空間為8字節(jié)和4字節(jié)。根據(jù)底層源碼對象分配的最小內(nèi)存空間為16可得,Person對象占據(jù)的內(nèi)存空間為16。此處值得注意的是,根據(jù)內(nèi)存對齊,也可得出Person對象占據(jù)16個字節(jié)。
Person對象內(nèi)存示意圖.jpeg

那么根據(jù)這個分析邏輯,Student對象占據(jù)的內(nèi)存空間是不是20或者24呢?答案當(dāng)然是不對,雖然Student結(jié)構(gòu)體中有Person類型的指針(16個字節(jié))和int類型成員變量_no(4個字節(jié)),但是要注意的是上面Person對象占據(jù)的16個字節(jié)是沒有完全占據(jù)完的,還留有4個字節(jié)的空白地址,系統(tǒng)會自動的把int類型成員變量_no放在這4個字節(jié)的空白地址中,所以Student對象也占據(jù)16個字節(jié)的內(nèi)存空間。

Student內(nèi)存示意圖.jpeg

內(nèi)存對齊:

  1. 前面的地址必須是后面的地址整數(shù)倍,不是就補齊。
  2. 整個結(jié)構(gòu)體的地址必須是最大字節(jié)的整數(shù)倍。

OC對象的分類

OC對象分為三種:
實例對象(instance對象)
類對象(class對象)
元類對象(meta-class對象)

1、實例對象(instance對象)

實例對象(instance對象)就是通過類的alloc出來的對象,每次調(diào)用alloc都會產(chǎn)生新的實例對象。例如:

NSObjcet *obj1 = [[NSObjcet alloc] init];
NSObjcet *obj2 = [[NSObjcet alloc] init];

obj1和obj2都是NSObject的實例對象,但是它們是不同的兩個實例對象,分別占用兩塊不同的內(nèi)存地址。
實例對象在內(nèi)存中存儲的信息包括:
1、isa指針
2、其他成員變量

實例對象在內(nèi)存中存儲信息示意圖.png

2、類對象(class對象)

類對象(class對象)就是通過class方法或者runtime的object_getClass方法得到的class對象

Class objClass1 = [obj1 class];
Class objClass2 = [obj2 class];
Class objClass3 = [NSObject class];

// runtime方法
Class objClass4 = object_getClass(obj1);
Class objClass5 = object_getClass(obj2);

objClass1-objClass5都是NSObject的類對象(class對象),且它們是同一個對象。

每個類在內(nèi)存中有且只有一個class對象

類對象在內(nèi)存中存儲的信息包括:
1、isa指針
2、superClass指針
3、類的屬性信息(@property),類的成員變量信息(ivar
4、類的對象方法信息(instance method),類的協(xié)議信息(protocol

類對象在內(nèi)存中存儲信息示意圖.png

3、元類對象(meta-class對象)

元類對象(meta-class對象)就是通過RunTime的object_getClass方法得到的對象

//通過RunTIme的API獲得元類對象
Class objectMetaClass = object_getClass([NSObject class]);

objectMetaClass就是NSObject的元類對象

每個類在內(nèi)存中有且只有一個元類對象

元類對象和類對象的內(nèi)存結(jié)構(gòu)是一樣的,但是用途不一樣,元類對象在內(nèi)存中存儲的信息包括:
1、isa指針
2、superClass指針
3、類的類方法信息(class method

元類對象在內(nèi)存中存儲信息示意圖.png

以上我們了解了實例對象、類對象和元類對象的含義以及包含的內(nèi)容,那么它們當(dāng)中的isa指針和superClass指針分別指向哪里呢?

isa指針的指向

isa指針指向用一張示意圖來簡單概括一下:


isa指針指向示意圖.jpeg

實例對象(instance對象)的isa指針指向class。當(dāng)調(diào)用對象方法時,通過實例對象的isa找到class,最后找到對象方法的實現(xiàn)進行調(diào)用
類對象(class對象)的isa指針指向meta-class。當(dāng)調(diào)用類方法時,通過類對象的isa找到meta-class,最后找到類方法的實現(xiàn)進行調(diào)用。

class的superClass指針的指向

class的superClass指針指向用一張示意圖來簡單概括一下:

superClass指針指向示意圖.jpeg

圖中舉例Student繼承自Person,Person繼承自NSObject。
當(dāng)Student的實例對象要調(diào)用父類Person的對象方法時,會先通過isa找到Student的class,然后通過class中的superClass找到父類Person的class,最后找到對象方法的實現(xiàn)進行調(diào)用。

meta-class對象的superClass指針指向

meat-class的superClass指針指向示意圖.jpeg

同上,當(dāng)Student的class要調(diào)用Person的類方法時,會先通過isa找到Student的meta-class,然后通過superClass找到Person的meta-class,最后找到類方法的實現(xiàn)進行調(diào)用。

這里當(dāng)然要提一下非常經(jīng)典的isa指向圖,做進一步的總結(jié):


超神圖.jpeg

1、instance的isa指向clas
2、class的isa指向meta-class
3、meta-class的isa指向基類的meta-class,基類的isa指向自己
4、class的superClass指向父類的class,如果沒有父類,則superClass指針為nil
5、meta-class的superClass指向父類的meta-class,基類的meta-class的superClass指向基類的class
6、instance調(diào)用對象方法的軌跡:通過isa找到class,方法不存在,就通過supercla逐層父類里找,有就實現(xiàn),如果找到基類仍沒有找到,就會拋出unrecognized selector sent to instance異常
7、class調(diào)用類方法的軌跡:通過isa找到meta-class,方法不存在,就通過superClass逐層父類里找。

補充:
相信很多人在查看源碼或者看一些底層博客的時候,經(jīng)常會看到下面一段代碼,來講述class的內(nèi)部結(jié)構(gòu):

typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

這段源碼其實講述的也是class內(nèi)部結(jié)構(gòu),包含成員變量列表、方法列表、方法緩存以及協(xié)議列表。細心的人可能會發(fā)現(xiàn),這段代碼里面是有if判斷條件的:

#if !__OBJC2__

判斷條件是非OC2.0版本,也就是說在OC2.0之前的版本中,class底層的結(jié)構(gòu)體中包含上面代碼所講述的,但我們現(xiàn)在所用的最新版肯定是OC2.0版本了,所以這段代碼就不再使用了。

新的class底層的objc_class結(jié)構(gòu)體:


objc_class結(jié)構(gòu)體.jpeg

可以看到結(jié)構(gòu)體中只包含isa、superclasscachebits。而bits經(jīng)過& FAST_DATA_MASK之后,會得到struct class_rw_t這樣一個結(jié)構(gòu)體:

class_rw_t.jpeg

rw代表readwrite,可讀可寫
t代表table,列表

struct class_rw_t結(jié)構(gòu)體中就包含了方法列表、屬性列表以及協(xié)議列表,這些都是可讀可寫的。其中還包含一個struct class_ro_t的結(jié)構(gòu)體:

class_ro_t.jpeg

ro代表readonly,只讀

struct class_ro_t的結(jié)構(gòu)體中包含了instance對象占用的內(nèi)存空間、類名以及成員變量列表,當(dāng)然這些都是只讀的。

總結(jié):

一個NSObject對象占用多少內(nèi)存?
答:系統(tǒng)會為一個NSObject對象分配最少16個字節(jié)的內(nèi)存空間。一個指針變量所占用的大?。?4bit占8個字節(jié),32bit占4個字節(jié))
對象的isa指針指向哪里?
答:instance對象的isa指針指向class對象,class對象的isa指針指向meta-class對象,meta-class對象的isa指針指向基類的meta-class對象,基類自己的isa指針指向自己。
OC的類信息存放在哪里?
答:成員變量的具體值存放在實例對象(instance對象);對象方法,協(xié)議,屬性,成員變量信息存放在類對象(class對象);類方法信息存放在元類對象(meta-class對象)。

本文如果對你有所幫助,點亮喜歡支持一下
更多底層知識,掃碼關(guān)注公眾號
iOS進階


我的二維碼.jpg
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容