天天看點

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

我們在前面探索了

iOS

中的對象原理,面向對象程式設計中有一句名言:

萬物皆對象

那麼對象又是從哪來的呢?有過面向對象程式設計基礎的同學肯定都知道是類派生出對象的,那麼今天我們就一起來探索一下類的底層原理吧。

一、

iOS

中的類到底是什麼?

我們在日常開發中大多數情況都是從

NSObject

這個基類來派生出我們需要的類。那麼在

OC

底層,我們的類

Class

到底被編譯成什麼樣子了呢?

我們建立一個

macOS

控制台項目,然後建立一個

Animal

類出來。

// Animal.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Animal : NSObject

@end

NS_ASSUME_NONNULL_END

// Animal.m
@implementation Animal

@end

// main.m
#import <Foundation/Foundation.h>
#import "Animal.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [[Animal alloc] init];
        NSLog(@"%p", animal);
    }
    return 0;
}
           

我們在終端執行

clang

指令:

clang -rewrite-objc main.m -o main.cpp
           

這個指令是将我們的

main.m

重寫成

main.cpp

,我們打開這個檔案搜尋

Animal

:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

我們發現有多個地方都出現了

Animal

:

// 1
typedef struct objc_object Animal;

// 2
struct Animal_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
};

// 3
objc_getClass("Animal")
           

我們先全局搜尋第一個

typedef struct objc_object

,發現有 843 個結果

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

我們通過

Command + G

快捷鍵快速翻閱一下,最終在 7626 行找到了

Class

的定義:

typedef struct objc_class *Class;
           

由這行代碼我們可以得出一個結論,

Class

類型在底層是一個結構體類型的指針,這個結構體類型為

objc_class

再搜尋

typedef struct objc_class

發現搜不出來了,這個時候我們需要在

objc4-756

源碼中進行探索了。

我們在

objc4-756

源碼中直接搜尋

struct objc_class

,然後定位到

objc-runtime-new.h

檔案

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_object

再次出現了,并且這次是作為

objc_class

的父類。這裡再次引用那句經典名言 萬物皆對象,也就是說類其實也是一種對象。

由此,我們可以簡單總結一下類和對象在

C

OC

中分别的定義

C OC
objc_object NSObject
objc_class NSObject(Class)

二、類的結構是什麼樣的呢?

通過上面的探索,我們已經知道了類本質上也是對象,而日常開發中常見的成員變量、屬性、方法、協定等都是在類裡面存在的,那麼我們是不是可以猜想在

iOS

底層,類其實就存儲了這些内容呢?

我們可以通過分析源碼來驗證我們的猜想。

從上一節中

objc_class

的定義處,我們可以梳理出

Class

中的 4 個屬性

  • isa

    指針
  • superclass

    指針
  • cache

  • bits

需要值得注意的是,這裡的

isa

指針在這裡是隐藏屬性.

2.1

isa

指針

首先是

isa

指針,我們之前已經探索過了,在對象初始化的時候,通過

isa

可以讓對象和類關聯,這一點很好了解,可是為什麼在類結構裡面還會有

isa

呢?看過上一篇文章的同學肯定知道這個問題的答案了。沒錯,就是元類。我們的對象和類關聯起來需要

isa

,同樣的,類和元類之間關聯也需要

isa

2.2

superclass

指針

顧名思義,

superclass

指針表明目前類指向的是哪個父類。一般來說,類的根父類基本上都是

NSObject

類。根元類的父類也是

NSObject

類。

2.3

cache

緩存

cache

的資料結構為

cache_t

,其定義如下:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    
    ...省略代碼...
}
           

類的緩存裡面存放的是什麼呢?是屬性?是執行個體變量?還是方法?我們可以通過閱讀

objc-cache.mm

源檔案來解答這個問題。

  • objc-cache.m
  • Method cache management
  • Cache flushing
  • Cache garbage collection
  • Cache instrumentation
  • Dedicated allocator for large caches

上面是

objc-cache.mm

源檔案的注釋資訊,我們可以看到

Method cache management

的出現,翻譯過來就是方法緩存管理。那麼是不是就是說

cache

屬性就是緩存的方法呢?而

OC

中的方法我們現在還沒有進行探索,先假設我們已經掌握了相關的底層原理,這裡先簡單提一下。

我們在類裡面編寫的方法,在底層其實是以

SEL

+

IMP

的形式存在。

SEL

就是方法的選擇器,而

IMP

則是具體的方法實作。這裡可以以書籍的目錄以及内容來類比,我們查找一篇文章的時候,需要先知道其标題(

SEL

),然後在目錄中看有沒有對應的标題,如果有那麼就翻到對應的頁,最後我們就找到了我們想要的内容。當然,

iOS

中方法要比書籍的例子複雜一些,不過暫時可以這麼簡單的了解,後面我們會深入方法的底層進行探索。

2.4

bits

屬性

bits

的資料結構類型是

class_data_bits_t

,同時也是一個結構體類型。而我們閱讀

objc_class

源碼的時候,會發現很多地方都有

bits

的身影,比如:

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

bool hasCustomRR() {
    return ! bits.hasDefaultRR();
}    

bool canAllocFast() {
    assert(!isFuture());
    return bits.canAllocFast();
}
           

這裡值得我們注意的是,

objc_class

data()

方法其實是傳回的

bits

data()

方法,而通過這個

data()

方法,我們發現諸如類的位元組對齊、

ARC

、元類等特性都有

data()

的出現,這間接說明

bits

屬性其實是個大容器,有關于記憶體管理、C++ 析構等内容在其中有定義。

這裡我們會遇到一個十分重要的知識點:

class_rw_t

data()

方法的傳回值就是

class_rw_t

類型的指針對象。我們在本文後面會重點介紹。

三、類的屬性存在哪?

上一節我們對

OC

中類結構有了基本的了解,但是我們平時最常打交道的内容-屬性,我們還不知道它究竟是存在哪個地方。接下來我們要做一件事情,就是在

objc4-756

的源碼中建立一個

Target

,為什麼不直接用上面的

macOS

指令行項目呢?因為我們要開始結合

LLDB

列印一些類的内部資訊,是以隻能是建立一個依靠于

objc4-756

源碼

project

target

出來。同樣的,我們還是選擇

macOS

的指令行作為我們的

target

接着我們建立一個類

Person

,然後添加一些執行個體變量和屬性出來。

// Person.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
@end

NS_ASSUME_NONNULL_END

// main.m
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        Class pClass = object_getClass(p);
        NSLog(@"%s", p);
    }
    return 0;
}
           

我們打一個斷點到

main.m

檔案中的

NSLog

語句處,然後運作剛才建立的

target

target

跑起來之後,我們在控制台先列印輸出一下

pClass

的内容:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

3.1 類的記憶體結構

我們這個時候需要借助指針平移來探索,而對于類的記憶體結構我們先看下面這張表格:

類的記憶體結構 大小(位元組)
isa 8
superclass 8
cache 16

前兩個大小很好了解,因為

isa

superclass

都是結構體指針,而在

arm64

環境下,一個結構體指針的記憶體占用大小為 8 位元組。而第三個屬性

cache

則需要我們進行抽絲剝繭了。

cache_t cache;

struct cache_t {
    struct bucket_t *_buckets; // 8
    mask_t _mask;  // 4
    mask_t _occupied; // 4
}

typedef uint32_t mask_t; 
           

從上面的代碼我們可以看出,

cache

屬性其實是

cache_t

類型的結構體,其内部有一個 8 位元組的結構體指針,有 2 個各為 4 位元組的

mask_t

。是以加起來就是 16 個位元組。也就是說前三個屬性總共的記憶體偏移量為 8 + 8 + 16 = 32 個位元組,32 是 10 進制的表示,在 16 進制下就是 20。

3.2 探索

bits

屬性

我們剛才在控制台列印輸出了

pClass

類對象的内容,我們簡單畫個圖如下所示:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

那麼,類的

bits

屬性的記憶體位址順理成章的就是在

isa

的初始偏移量位址處進行 16 進制下的 20 遞增。也就是

0x1000021c8 + 0x20 = 0x1000021e8
           

我們嘗試列印這個位址,注意這裡需要強轉一下:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

這裡報錯了,問題其實是出在我們的

target

沒有關聯上

libobjc.A.dylib

這個動态庫,我們關聯上重新運作項目

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

我們重複一遍上面的流程:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

這一次成功了。在

objc_class

源碼中有:

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

我們不妨列印一下裡面的内容:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

傳回了一個

class_rw_t

指針對象。我們在

objc4-756

源碼中搜尋

class_rw_t

:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    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;
    
    ...省略代碼...    
}
           

顯然的,

class_rw_t

也是一個結構體類型,其内部有

methods

properties

protocols

等我們十分熟悉的内容。我們先猜想一下,我們的屬性應該存放在

class_rw_t

properties

裡面。為了驗證我們的猜想,我們接着進行

LLDB

列印:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

我們再接着列印

properties

:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

properties

居然是空的,難道是 bug?其實不然,這裡我們還漏掉了一個非常重要的屬性

ro

。我們來到它的定義:

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;

    ...隐藏代碼...    
}
           

ro

的類型是

class_ro_t

結構體,它包含了

baseMethodList

baseProtocols

ivars

baseProperties

等屬性。我們剛才在

class_rw_t

中沒有找到我們聲明在

Person

類中的執行個體變量

hobby

和屬性

nickName

,那麼希望就在

class_ro_t

身上了,我們列印看看它的内容:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

根據名稱我們猜測屬性應該在

baseProperties

裡面,我們列印看看:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

Bingo! 我們的屬性

nickName

被找到了,那麼我們的執行個體變量

hobby

呢?我們從 $8 的 count 為 1 可以得知肯定不在

baseProperites

裡面。根據名稱我們猜測應該是在

ivars

裡面。

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

哈哈,

hobby

執行個體變量也被我們找到了,不過這裡的

count

為什麼是 2 呢?我們列印第二個元素看看:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

結果為

_nickName

。這一結果證明了編譯器會幫助我們給屬性

nickName

生成一個帶下劃線字首的執行個體變量

_nickName

至此,我們可以得出以下結論:

class_ro_t

是在編譯時就已經确定了的,存儲的是類的成員變量、屬性、方法和協定等内容。

class_rw_t

是可以在運作時來拓展類的一些屬性、方法和協定等内容。

四、類的方法存在哪?

研究完了類的屬性是怎麼存儲的,我們再來看看類的方法。

我們先給我們的

Person

類增加一個

sayHello

的執行個體方法和一個

sayHappy

的類方法。

// Person.h
- (void)sayHello;
+ (void)sayHappy;

// Person.m
- (void)sayHello
{
    NSLog(@"%s", __func__);
}

+ (void)sayHappy
{
    NSLog(@"%s", __func__);
}
           

按照上面的思路,我們直接讀取

class_ro_t

中的

baseMethodList

的内容:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

sayHello

被列印出來了,說明

baseMethodList

就是存儲執行個體方法的地方。我們接着列印剩下的内容:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

可以看到

baseMethodList

中除了我們的執行個體方法

sayHello

外,還有屬性

nickName

getter

setter

方法以及一個

C++

析構方法。但是我們的類方法

sayHappy

并沒有被列印出來。

五、類的類方法存在哪?

我們上面已經得到了屬性,執行個體方法的是怎麼樣存儲,還留下了一個疑問點,就是類方法是怎麼存儲的,接下來我們用

Runtime

的 API 來實際測試一下。

// main.m
void testInstanceMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));

    Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        Class pClass = object_getClass(p);
        
        testInstanceMethod_classToMetaclass(pClass);
        NSLog(@"%p", p);
    }
    return 0;
}
           

運作後列印結果如下:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

首先

testInstanceMethod_classToMetaclass

方法測試的是分别從類和元類去擷取執行個體方法、類方法的結果。由列印結果我們可以知道:

  • 對于類對象來說,

    sayHello

    是執行個體方法,存儲于類對象的記憶體中,不存在于元類對象中。而

    sayHappy

    是類方法,存儲于元類對象的記憶體中,不存在于類對象中。
  • 對于元類對象來說,

    sayHello

    是類對象的執行個體方法,跟元類沒關系;

    sayHappy

    是元類對象的執行個體方法,是以存在元類中。

我們再接着測試:

// main.m
void testClassMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(sayHello));
    Method method2 = class_getClassMethod(metaClass, @selector(sayHello));

    Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        Class pClass = object_getClass(p);
        
        testClassMethod_classToMetaclass(pClass);
        NSLog(@"%p", p);
    }
    return 0;
}
           

運作後列印結果如下:

iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

從結果我們可以看出,對于類對象來說,通過

class_getClassMethod

擷取

sayHappy

是有值的,而擷取

sayHello

是沒有值的;對于元類對象來說,通過

class_getClassMethod

擷取

sayHappy

也是有值的,而擷取

sayHello

是沒有值的。這裡第一點很好了解,但是第二點會有點讓人糊塗,不是說類方法在元類中是展現為對象方法的嗎?怎麼通過

class_getClassMethod

從元類中也能拿到

sayHappy

,我們進入到

class_getClassMethod

方法内部可以解開這個疑惑:

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

Class getMeta() {
    if (isMetaClass()) return (Class)this;
    else return this->ISA();
}    
           

可以很清楚的看到,

class_getClassMethod

方法底層其實調用的是

class_getInstanceMethod

,而

cls->getMeta()

方法底層的判斷邏輯是如果已經是元類就傳回,如果不是就傳回類的

isa

。這也就解釋了上面的

sayHappy

為什麼會出現在最後的列印中了。

除了上面的

LLDB

列印,我們還可以通過

isa

的方式來驗證類方法存放在元類中。

  • 通過 isa 在類對象中找到元類
  • 列印元類的 baseMethodsList

具體的過程筆者不再贅述。

六、類和元類的建立時機

我們在探索類和元類的時候,對于其建立時機還不是很清楚,這裡我們先抛出結論:

  • 類和元類是在編譯期建立的,即在進行 alloc 操作之前,類和元類就已經被編譯器建立出來了。

那麼如何來證明呢,我們有兩種方式可以來證明:

  • LLDB

    列印類和元類的指針
iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結
  • 編譯項目後,使用

    MachoView

    打開程式二進制可執行檔案檢視:
iOS 底層探索 - 類一、iOS 中的類到底是什麼?二、類的結構是什麼樣的呢?三、類的屬性存在哪?四、類的方法存在哪?五、類的類方法存在哪?六、類和元類的建立時機七、總結

七、總結

  • 類和元類建立于編譯時,可以通過

    LLDB

    來列印類和元類的指針,或者

    MachOView

    檢視二進制可執行檔案
  • 萬物皆對象:類的本質就是對象
  • 類在

    class_ro_t

    結構中存儲了編譯時确定的屬性、成員變量、方法和協定等内容。
  • 執行個體方法存放在類中
  • 類方法存放在元類中

我們完成了對

iOS

中類的底層探索,下一章我們将對類的緩存進行深一步探索,敬請期待~