OC對象的本質

本次講解的很多內容都涉及到objc的源碼,有興趣的可以去下載最新版本的objc4源碼。

1. OC對象的內存布局

1.1 一個NSObject對象占多少內存?

我們平時開發中說用到了絕大多數的類都是以NSObject作為基類。我們進入NSObject.h文件可以看到NSObject類的定義如下:

@interface NSObject <NSObject> {
    Class isa ;
}

我們將OC代碼轉成c/c++代碼后可以看到,NSObject類是通過結構體來實現的,如下所示:

struct NSObject_IMPL {
    Class isa;
};

// Class的定義
typedef struct object_class *Class;

從上面可以看出這個結構體和OC中NSObject類的定義是一致的。這個類中只包含一個Class類型的屬性,而Class是一個指向object_class結構體的指針(結構體的地址就是結構體中第一個成員的地址),所以isa就是一個指針,占8個字節(64位機器上面)。那么,是不是意味著一個NSObject對象在內存中就占8個字節呢?我們通過代碼測試一下:

NSObject *obj = [[NSObject alloc] init];

// class_getInstanceSize()函數需要引入頭文件#import <objc/runtime.h>    
NSLog(@"---%zd",class_getInstanceSize([obj class]));

// malloc_size()函數需要引入頭文件#import <malloc/malloc.h>
NSLog(@"---%zd",malloc_size((__bridge const void *)(obj)));
    
    
// ***************打印結果***************
2020-01-03 11:48:21.302884+0800 AppTest[62149:5950838] ---8
2020-01-03 11:48:21.303065+0800 AppTest[62149:5950838] ---16
  • class_getInstanceSize()函數得到的結果和我們預期是一致的,這個函數是runtime的一個函數,它返回的是類的一個實例的大小。我們查看objc4源碼可以看到這個函數返回的是類的成員變量所占內存的大小(是內存對齊后的大小,結構體內存對齊的規則是結構體總大小必須是結構體中最大成員所占內存大小的倍數),所以得到的結果是8.
  • malloc_size()函數返回的是系統實際分配的內存大小,是16個字節,但是實際使用的只有8個字節。

所以,一個NSObject對象所占用的內存是16個字節。為什么會分配16個字節呢?我們可以去objc4源碼看看alloc方法(在NSObject.mm文件中)的調用流程:

alloc-->_objc_rootAlloc()-->callAlloc()-->class_createInstance()-->_class_createInstanceFromZone()。

_class_createInstanceFromZone()函數中調用了instanceSize()來確定一個對象要分配的內存的大小,其函數實現如下:

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

可以看到給一個對象分配內存的大小最小為16(這是系統硬性規定)。另外要注意的是一個實例對象占用多少內存和類中是否有方法是沒有關系的,因為類中的方法并不存放在實例對象中。

1.2 自定義對象占多少內存?

我們先定義一個繼承自NSObjectStudent類,Student類聲明了2個int類型屬性:

@interface Student : NSObject

@property (nonatomic , assign) int age;
@property (nonatomic , assign) int height;

@end

我們將其轉換為c/c++代碼查看其底層結構:

struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int age;
    int height;
};

struct NSObject_IMPL {
    Class isa;
};

Student_IMPL結構體的第一個成員就是其父類的結構體,從上面我們可以得到2個信息:子類的成員變量包含了父類的成員變量;父類成員變量放在子類成員變量的前面。所以Student類有3個成員變量:isa(8字節)、age(4字節)和height(4字節)。那一個Student的實例對象是不是占16個字節呢?下面我們測試一下:

Student *stu = [[Student alloc] init];
NSLog(@"%zd",malloc_size((__bridge const void *)(stu)));

// ***************打印結果***************
2020-01-03 17:36:47.773438+0800 CommandLine[62909:6064600] 16

如果此時我們在Student類中新增一個int類型的屬性weight,那一個Student的實例對象是不是就占20個字節呢?測試發現結果并不是20,而是32。為什么會這樣呢,這就涉及到了iOS的內存字節對齊,其結果就是系統給一個對象分配的內存大小都是16的倍數。所以系統給一個自定義類的實例對象分配內存時,先計算類的所有成員變量(包括父類以及整個繼承鏈的成員變量)的大小size,如果size剛好是16的倍數,那分配的內存的大小就是size;如果size不是16的倍數,那就將size補齊到剛好是16的倍數為止,補齊后的結果就是實際分配的內存大小。(結構體內存對齊字節數是8,OC對象內存對齊字節數是16,有關iOS系統分配內存時的對齊規則可以查看libmalloc庫中的malloc.c文件中的malloc_zone_calloc()函數)。

為什么要進行內存對齊呢?簡單來說就是未對齊的數據會大大降低CPU的性能。因為CPU讀取數據時不是一個字節一個字節進行讀取的,而是每次讀取一塊數據,塊的大小在不同的系統上是不一樣的,可以是2、4、8、16個字節。比如說如果CPU一次讀取16個字節,如果一個對象占用內存的大小不是16的倍數,那么CPU讀取這個對象數據時就需要做一些額外的操作,影響CPU的性能。

2. OC對象的分類

前面提到的對象都是實例對象,OC中除了實例對象之外,還有另外兩種對象:類對象元類對象。

2.1 實例對象

實例對象就是通過類alloc出來的對象,比如Student *student = [[Student alloc] init];,這樣就創建了Student類的一個實例對象,每次調用alloc都會產生新的實例對象。

一個實例對象在內存中存儲的信息前面也提到了,它的內存結構是比較簡單的,就只存了實例對象的所有成員變量的數據:

  • isa指針(isa指針也是成員變量,只是它比較特殊,所有OC對象都有isa指針,具體的后面會介紹)。
  • 其他成員變量

2.2 類對象

OC中每個類都有一個與之對應的類對象,而且有且只有一個類對象。與實例對象相比,類對象的內存結構要復雜很多(關于類的底層數據結構后面再做介紹),其在內存中存儲的信息主要包括:

  • isa指針
  • superclass指針
  • 屬性信息
  • 對象方法(-開頭的方法)信息
  • 協議信息
  • 成員變量信息(這里存儲的是成員變量名字、類型等信息,這個和實例對象中存儲的成員變量數據不是一個概念)。
  • 其它一些信息。

獲取類對象的方法有多種,不管哪一種方法獲取到的類對象都是一樣的。

    Student *stu = [[Student alloc] init];
    
    // 1. 調用實例對象的class方法來獲取
    Class stuClass1 = [stu class];
    
    // 2. 調用類的class方法來獲取
    Class stuClass2 = [Student class];
    
    // 3. 調用runtime的object_getClass(object1);
    Class stuClass3 = object_getClass(stu);

注意第3種方法不要寫錯了,runtime中還有另外一個很相似的函數:

// 上面用的是這個函數,傳入一個,返回這個對象所屬的類
Class object_getClass(id _Nullable obj);

// 這個方法是傳入一個字符串,返回類名是這個字符串的類
Class objc_getClass(const char * _Nonnull name);

2.3 元類對象

從上面介紹我們可以看出,類對象是用來存儲實例對象的信息的(比如實例方法、屬性等信息),那類對象的信息(比如類的類方法信息)又是存在哪里呢?這就是我們要介紹的元類對象。

每個類在內存中有且只有一個元類對象,元類對象和類對象的內存結構是一樣的,只是具體存儲的信息不同,用途也不同。元類對象存儲的信息主要包括:

  • isa 指針
  • superclass 指針
  • 類方法(+開頭的方法)信息
  • 其他一些信息

獲取元類對象也是調用object_getClass()函數,只是傳入參數是類對象。換句話說object_getClass()函數傳入的是實例對象的話就返回類對象,傳入的是類對象的話就返回元類對象。

// 獲取元類對象
Class metaClass = object_getClass([Student class]);

// 判斷某個對象是否是元類對象
BOOL isMetaClass = class_isMetaClass([Student class]);

這里要注意[[Student class] class]這種寫法,[Student class]返回的是類對象,那類對象再調用class方法是不是就返回的是元類對象呢?答案是否定的,一個類對象調用class方法返回的就是它自身。

3. isa指針和superclass指針

從前面介紹可以看出,所有繼承自NSObject的對象都有isa指針,所有類對象和元類對象都有superclass指針。那這兩種指針到底有什么用呢?

我們首先來了解一下OC的方法調用原理,這屬于runtime的知識,這里只是簡單介紹一下,不做深入講解。調用OC方法底層是通過c語言的發送消息機制來實現的,比如一個實例對象stu調用study方法[stu study],其底層就是給stu對象發送消息(objc_msgSend(stu, @selector(study)))。但是study方法的相關信息并不是存儲在實例對象中,而是在類對象中,那實例對象如何查找到study方法呢?這里isa指針就起作用了,實例對象的isa指針就是指向實例所屬的類對象的(嚴格來說,isa指針并不是一個普通的指針,它里面存儲的信息除了類對象的地址外,還包括很多其他信息,這里不做深入講解,我們簡單理解為實例對象的isa就是指向類對象即可)。

實例對象通過isa指針找到了類對象,然后在類對象中查找study方法并執行。但是如果study方法是Student的父類實現的,那么在Student類中是找不到study方法的,此時就要根據superclass指針找到父類對象(superclass指針存儲的就是父類的地址,這和isa指針是不一樣的),如果父類也找不到那就繼續沿著繼承鏈進行查找。如果一直找到NSObject基類都沒找到的話,就會拋出unrecognized selector異常(這里不考慮runtime的消息轉發)。

對于類方法的調用也是一樣的流程,只不過是從給實例對象發消息變成了給類對象發消息。類對象會根據自己的isa指針找到元類對象,然后在元類對象中查找類方法,查找不到也是根據元類的superclass指針沿著繼承鏈查找。

isa指針和superclass指針的指向可以總結為下面一張圖:


對象、類、元類
  • 實例對象的isa指針指向該對象的類,該對象的實例方法保存在這個類的繼承鏈中;
  • 類對象的isa指針指向該類的元類,類方法保存在元類的繼承鏈中;
  • 元類和普通類一樣也有父類,也具備自己的繼承關系鏈,一個元類的父類就是這個元類的類的父類的元類(有點繞);
  • 所有元類的isa指針都是指向元類的根類(包括元類的根類的isa指針也是指向它自己);
  • 注意根元類的super_class指針指向的是根類(NSObject)。

最后一點要格外注意,舉個例子,如果一個以NSObject為基類的類MyClss,MyClass中聲明了一個類方法+(void)myTest;,但是并沒有實現這個類方法(整個繼承鏈上都沒有實現),如果我們調用[MyClass myTest]的話是會報unrecognized selector異常的。

但是,如果我們給NSObject添加一個分類,在分類中實現了一個實例方法-(void)myTest;,此時再調用[MyClass myTest]的話時能正常運行的,而且執行的就是分類中添加的實例方法-(void)myTest;。這個其實可以用上面那張圖進行解釋:首先MyClass類對象會根據其isa指針找到其元類對象,然后在元類對象和元類的繼承鏈上進行查找,一直查找到根元類對象都沒有找到一個名叫myTest的方法,然后跟元類又會沿著其superclass指針找到NSObject類對象,而NSObject類對象中剛好有個叫myTest的方法,所以就直接執行這個方法。

4. 類對象和元類對象的存儲結構(objc_class)

前面已經提過,不管是類對象還是元類對象,它們在內存中的存儲結構是一樣的。相關信息在objc4源碼中。下面我會列出一些關鍵信息,想要了解完整信息可以去查看源碼。


首先我們來看下objc_class這個結構體(這是c++語法,結構體可以繼承也可以在結構體里面定義函數),這個結構體我只列出了部分信息:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
}

可見objc_class繼承自objc_object,而objc_object結構體里面就只有isa這一個成員。我們再來看看objc_class里面的內容:

  • superclass指針
  • cache,方法緩存。是cache_t的結構體,這個結構體的定義可以去看源碼。
  • bits是一個class_data_bits_t結構體,這個結構體詳細信息可以去看源碼,這里我們主要介紹它后面的那個函數bits.data(),看源碼可知,這個函數的實現其實就是bits & FAST_DATA_MASK),這個操作就是取出bits的某些位得到的就是一個指向結構體的指針,也就是class_rw_t這個結構體。

下面我們來看看class_rw_t這個結構體(rw其實就是readwrite的意思,也就是表示類中可讀可寫的信息):

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    // 類中的只讀信息(詳細內容見后面介紹)
    const class_ro_t *ro;
    
    // 方法列表(如果是類對象這里就是實例方法列表,如果是元類對象這里就是類方法列表)
    method_array_t methods;
    
    // 屬性列表
    property_array_t properties;
    
    // 協議列表
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
    }

下面我們再來看看class_ro_t這個結構體(ro其實就是readonly的意思,也就是表示類中只讀的信息):

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; // 實例對象占用內存的大小
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name; // 類名
    method_list_t * baseMethodList; // 方法列表
    protocol_list_t * baseProtocols; // 協議列表
    const ivar_list_t * ivars; // 成員變量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties; // 屬性列表
    }

我們發現在class_rw_tclass_ro_t都有方法列表、屬性列表和協議列表,比如在class_rw_t中的方法列表是methods,在class_ro_t中的方法列表是baseMethodList,那這兩個有什么區別呢?class_ro_t的初始化是在編譯的過程中完成的,對于一個類對象來說,編譯完成后,class_ro_t中的baseMethodList存著實例方法列表,這部分內容是不可以修改的,當class_rw_t進行初始化時,會先將baseMethodList拷貝放入methods中,之后程序運行過程中動態添加的方法也是存放在methods中。對于屬性列表和協議列表也是一樣的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容

  • 今天是什么日子 起床:十點多 就寢:兩點多 天氣:晴 心情:美美噠 紀念日:高中畢業后的第一次來看玲兒 任務清單 ...
    文刀俠閱讀 79評論 0 0
  • 昨天晚上想 人有該做的事,想做的事,不得不做的事 為什么不能像吃飯睡覺一樣去做該做的事 比如,學習 讓它成為一個理...
    太陽當空照1閱讀 322評論 0 0
  • 這幾天每天都會抽時間觀看一期的論點,就一個觀點正反方開始辯論。昨天一個不小心,看到了羅胖參與的一期,辯題是討論關于...
    小豬豬同學閱讀 588評論 2 1
  • 忙里偷閑,準備寫上幾句話。 重溫《搞定》系列書,一直以來,我對《搞定》里面提倡的“下一步”清單,理解的一直不是很透...
    小李非刀閱讀 3,104評論 23 171