Runtime中的 isa 結構體

acBool發表於2019-03-19

原文連結

有一定經驗的iOS開發者,或多或少的都聽過Runtime。Runtime,也就是執行時,是Objective-C語言的特性之一。日常開發中,可能直接和Runtime打交道的機會不多。然而,"發訊息"、"訊息轉發"這些名詞開發者應該經常聽到,這些名詞所用到的技術基礎就是Runtime。瞭解Runtime,有助於開發者深入理解Objective-C這門語言。

在具體瞭解Runtime之前,先提一個問題,什麼是動態語言?

Objective-C是一門動態語言

使用Objective-C做iOS開發的同學一定都聽說過一句話:Objective-C是一門動態語言。動態語言,肯定是和靜態語言相對應的。那麼,靜態語言有哪些特性,動態語言又有哪些特性?

回顧一下大學時期,學的第一門語言C語言,學習C語言的過程中從來沒聽說過執行時,也沒聽說過什麼靜態語言,動態語言。因此我們有理由相信,C語言是一門靜態語言。

事實上也確實如此,C語言是一門靜態語言,Objective-C是一門動態語言。然而,還是說不出靜態語言和動態語言到底有什麼區別……

靜態語言和動態語言

靜態語言,可以理解成在編譯期間就確定一切的語言。以C語言來舉例,C語言編譯後會成為一個可執行檔案。假設我們在C程式碼中寫了一個hello函式,並且在主程式中呼叫了這個hello函式。倘若在編譯期間,hello函式的入口地址相對於主程式入口地址的偏移量是0x0000abcdef(不要在意這個值,只是用來舉例),那麼在執行該程式時,執行到hello函式時,一定執行的是相對主程式入口地址偏移量為0x0000abcdef的程式碼塊。也就是說,靜態語言,在編譯期間就已經確定一切,執行期間只是遵守編譯期確定的指令在執行

作為對比,再看一下動態語言,以經常用到的Objective-C為例。假設在Objective-C中寫了hello方法,並且在主程式中呼叫了hello方法,也就是傳送hello訊息。在編譯期間,只能確定要向某個物件傳送hello訊息,但是具體執行哪個記憶體塊的程式碼是不確定的,具體執行的程式碼需要在執行期間才能確定

到這裡,靜態語言和動態語言的區別已經很明顯了。靜態語言在編譯期間就已經確定一切,而動態語言編譯期間只能確定一部分,還有一部分需要在執行期間才能確定。也就是說,動態語言成為一個可執行程式並能夠正確的執行,除了需要一個編譯器外,還需要一套執行時系統,用於確定到底執行哪一塊程式碼。Objective-C中的執行時系統內就是Runtime。

Runtime原始碼

Runtime原始碼是一套用C語言實現的API,整套程式碼是開源的,可以從蘋果開源網站上下載Runtime原始碼。預設下載的Runtime原始碼是不能編譯的,通過修改配置和匯入必要的標頭檔案,可以編譯成功Runtime原始碼。我在github上放了編譯成功的Runtime原始碼,且有我在看Runtime原始碼時的一些註釋,本篇文章中的程式碼也是基於此Runtime原始碼。

由於Runtime原始碼程式碼量比較大,一篇文章介紹完Runtime原始碼是不可能的。因此這篇文章主要介紹Runtime中的isa結構體,作為Runtime的入門。

isa結構體

有經驗的iOS開發者可能都聽過一句話:在Objective-C語言中,類也是物件,且每個物件都包含一個isa指標,isa指標指向該物件所屬的類。不過現在Runtime中的物件定義已經不是這樣了,現在使用的是isa_t型別的結構體。每一個物件都有一個isa_t型別的結構體isa。之前的isa指標作用是指向該物件的類,那麼isa結構體作為isa指標的替代者,是如何完成這個功能的呢?

在解決這個問題之前,我們先來看一下Runtime原始碼中物件和類的定義。

objc_object

看一下Runtime中對id型別的定義

typedef struct objc_object *id;
複製程式碼

這裡的id也就是Objective-C中的id型別,代表任意物件,類似於C語言中的 void *。可以看到,*id實際上是一個指向結構體objc_object的指標。

再來看一下objc_object的定義,該定義位於objc-private.h檔案中:

struct objc_object {
    // isa結構體
private:
    isa_t isa;
}
複製程式碼

結構體中還包含一些public的方法。可以看到,物件結構體(objc_object)中的第一個變數就是isa_t 型別的isa。關於isa_t具體是什麼,後續再介紹。

Objective-C語言中最主要的就是物件和類,看完了物件在Runtime中的定義,再看一下類在Runtime中的定義。

objc_class

Runtime中對於Class的定義

typedef struct objc_class *Class;
複製程式碼

Class實際上是一個指向objc_class結構體的指標。

看一下結構體objc_class的定義,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
}
複製程式碼

結構體中還包含一些方法。

注意,objc_class是繼承於objc_object的,因此objc_class中也包含isa_t型別的isa。objc_class的定義可以理解成下面這樣:

struct objc_class {
    isa_t isa;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}
複製程式碼

isa的作用

上面也提到了,isa能夠使該物件找到自己所屬的類。為什麼物件需要知道自己所屬的類呢?這主要是因為物件的方法是儲存在該物件所屬的類中的。

這一點是很容易理解的,一個類可以有多個物件,倘若每個物件都含有自己能夠執行的方法,那對於記憶體來說是災難級的。

在向物件傳送訊息,也就是例項方法被呼叫時,物件通過自己的isa找到所屬的類,然後在類的結構中找到對應方法的實現(關於在類結構中如何找到方法的實現,後續的文章再介紹)。

我們知道,Objective-C中區分類方法和例項方法。例項方法是如何找到的我們瞭解了,那麼類方法是如何找到的呢?類結構體中也有isa,類物件的isa指向哪裡呢?

元類(metaClass)

為了解決類方法呼叫,Objective-C引入了元類(metaClass),類物件的isa指向該類的元類,一個類物件對應一個元類物件。

元類物件也是類物件,既然是類物件,那麼元類物件中也有isa,那麼元類的isa又指向哪裡呢?總不能指向元元類吧……這樣是無窮無盡的。

Objective-C語言的設計者已經考慮到了這個問題,所有元類的isa都指向一個元類物件,該元類物件就是 meta Root Class,可以理解成根元類。關於例項物件、類、元類之間的關係,蘋果官方給了一張圖,非常清晰的表明了三者的關係,如下

image

isa結構體定義

瞭解了isa的作用,現在來看一下isa的定義。isa是isa_t型別,isa_t也是一個結構體,其定義在objc-private.h中:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    // 相當於是unsigned long bits;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
複製程式碼

ISA_BITFIELD的定義在 isa.h檔案中:

uintptr_t nonpointer        : 1;                                         \
uintptr_t has_assoc         : 1;                                         \
uintptr_t has_cxx_dtor      : 1;                                         \
uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic             : 6;                                         \
uintptr_t weakly_referenced : 1;                                         \
uintptr_t deallocating      : 1;                                         \
uintptr_t has_sidetable_rc  : 1;                                         \
uintptr_t extra_rc          : 8
複製程式碼

注意:這裡的程式碼都是x86_64架構下的,arm64架構下和x86_64架構下有區別,但是不影響我們理解isa_t結構體。

將isa_t結構體中的ISA_BITFIELD使用isa.h檔案中的ISA_BITFIELD替換,isa_t的定義可以表示如下:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    // 相當於是unsigned long bits;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        uintptr_t nonpointer        : 1; 
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44;
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
    };
#endif
};
複製程式碼

注意isa_t是聯合體,也就是說isa_t中的變數,cls、bits和內部的結構體全都位於同一塊地址空間。

本篇文章主要分析下isa_t中內部結構體中各個變數的作用

struct {
    uintptr_t nonpointer        : 1; 
    uintptr_t has_assoc         : 1;
    uintptr_t has_cxx_dtor      : 1;
    uintptr_t shiftcls          : 44;
    uintptr_t magic             : 6;
    uintptr_t weakly_referenced : 1;
    uintptr_t deallocating      : 1;
    uintptr_t has_sidetable_rc  : 1;
    uintptr_t extra_rc          : 8;
};
複製程式碼

該結構體共佔64位,其記憶體分佈如下:

image

在瞭解內個結構體各個變數的作用前,先通過Runtime程式碼看一下isa結構體是如何初始化的。

isa結構體初始化

isa結構體初始化定義在objc_object結構體中,看一下官方提供的函式和註釋:

// initIsa() should be used to init the isa of new objects only.
// If this object already has an isa, use changeIsa() for correctness.
// initInstanceIsa(): objects with no custom RR/AWZ
// initClassIsa(): class objects
// initProtocolIsa(): protocol objects
// initIsa(): other objects
void initIsa(Class cls /*nonpointer=false*/);
void initClassIsa(Class cls /*nonpointer=maybe*/);
void initProtocolIsa(Class cls /*nonpointer=maybe*/);
void initInstanceIsa(Class cls, bool hasCxxDtor);
複製程式碼

官方提供的有類物件初始化isa,協議物件初始化isa,例項物件初始化isa,其他物件初始化isa,分別對應不同的函式。

看下每個函式的實現:

inline void objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}

inline void objc_object::initClassIsa(Class cls)
{
    if (DisableNonpointerIsa  ||  cls->instancesRequireRawIsa()) {
        initIsa(cls, false/*not nonpointer*/, false);
    } else {
        initIsa(cls, true/*nonpointer*/, false);
    }
}

inline void objc_object::initProtocolIsa(Class cls)
{
    return initClassIsa(cls);
}

inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    assert(!cls->instancesRequireRawIsa());
    assert(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}
複製程式碼

可以看到,無論是類物件,例項物件,協議物件,還是其他物件,初始化isa結構體最終都呼叫了

inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
複製程式碼

函式,只是所傳的引數不同而已。

最終呼叫的initIsa函式的程式碼,經過簡化後如下:

inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        // 例項物件的isa初始化直接走else分之
        // 初始化一個心得isa_t結構體
        isa_t newisa(0);
        // 對新結構體newisa賦值
        // ISA_MAGIC_VALUE的值是0x001d800000000001ULL,轉化成二進位制是64位
        // 根據註釋,使用ISA_MAGIC_VALUE賦值,實際上只是賦值了isa.magic和isa.nonpointer
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        // 將當前物件的類指標賦值到shiftcls
        // 類的指標是按照位元組(8bits)對齊的,其指標後三位都是沒有意義的0,因此可以右移3位
        newisa.shiftcls = (uintptr_t)cls >> 3;
        // 賦值。看註釋這個地方不是執行緒安全的??
        isa = newisa;
    }
}
複製程式碼

初始化例項物件的isa時,傳入的nonpointer引數是true,所以直接走了else分之。在else分之中,對isa的bits分之賦值ISA_MAGIC_VALUE。根據註釋,這樣程式碼實際上只是對isa中的magic和nonpointer進行了賦值,來看一下為什麼。

ISA_MAGIC_VALUE的值是0x001d800000000001ULL,轉化成二進位制就是0000 0000 0001 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001,將每一位對應到isa內部的結構體中,看一下對哪些變數產生了影響:

image

可以看到將nonpointer賦值為1;將magci賦值為110111;其他的仍然都是0。所以說只賦值了isa.magci和isa.nonpointer。

nonpointer

在文章開頭也提到了,在Objective-C語言中,類也是物件,且每個物件都包含一個isa指標,現在改為了isa結構體。nonpointer作用就是區分這兩者。

  1. 如果nonpointer為1,代表不是isa指標,而是isa結構體。雖然不是isa指標,但是通過isa結構體仍然能獲得類指標(下面會分析)。
  2. 如果nonpointer為0,代表當前是isa指標,訪問物件的isa會直接返回類指標。
magic

magic的值偵錯程式會用到,偵錯程式根據magci的值判斷當前物件已經初始過了,還是尚未初始化的空間。

has_cxx_dtor

接下來就是對has_cxx_dtor進行賦值。has_cxx_dtor表示當前物件是否有C++的解構函式(destructor),如果沒有,釋放時會快速的釋放記憶體。

shiftcls

在函式

inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
複製程式碼

中,引數cls就是類的指標。而

newisa.shiftcls = (uintptr_t)cls >> 3;
複製程式碼

shiftcls儲存的到底是什麼呢?

實際上,shiftcls儲存的就是當前物件類的指標。之所以右移三位是出於節省空間上的考慮。

在Objective-C中,類的指標是按照位元組(8 bits)對齊的,也就是說類指標地址轉化成十進位制後,都是8的倍數,也就是說,類指標地址轉化成二進位制後,後三位都是0。既然是沒有意義的0,那麼在儲存時就可以省略,用節省下來的空間儲存一些其他資訊。

在objc-runtime-new.mm檔案的

static __attribute__((always_inline)) id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
複製程式碼

函式,類初始化時會呼叫該函式。可以在該函式中列印類物件的地址

if (!cls) return nil;
// 這裡可以列印類指標的地址,類指標地址最後一位是十六進位制的8或者0,說明
// 類指標地址後三位都是0
printf("cls address = %p\n",cls);
複製程式碼

列印出的部分資訊如下:

cls address = 0x7fff83bca218
cls address = 0x7fff83bcab28
cls address = 0x7fff83bc5290
cls address = 0x7fff83717f58
cls address = 0x7fff83717f58
cls address = 0x100b15140
cls address = 0x7fff83717fa8
cls address = 0x7fff837164c8
cls address = 0x7fff837164c8
cls address = 0x7fff83716e78
cls address = 0x100b15140
cls address = 0x7fff837175a8
cls address = 0x7fff837175a8
cls address = 0x7fff83717fa8
複製程式碼

可以看到類物件的地址最後一位都是8或者0,說明類物件確實是按照位元組對齊,後三位都是0。因此在賦值shiftcls時,右移三位是安全的,不會丟失類指標資訊。

我們可以寫程式碼驗證一下物件的isa和類物件指標的關係。程式碼如下:

#import <Foundation/Foundation.h>
#import "objc-runtime.h"

// 把一個十進位制的數轉為二進位制
NSString * binaryWithInteger(NSUInteger decInt){
    NSString *string = @"";
    NSUInteger x = decInt;
    while(x > 0){
        string = [[NSString stringWithFormat:@"%lu",x&1] stringByAppendingString:string];
        x = x >> 1;
    }
    return string;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 把物件轉為objc_object結構體
        struct objc_object *object = (__bridge struct objc_object *)([NSObject new]);
        NSLog(@"binary = %@",binaryWithInteger(object->isa));
        // uintptr_t實際上就是unsigned long
        NSLog(@"binary = %@",binaryWithInteger((uintptr_t)[NSObject class]));
    }
    return 0;
}
複製程式碼

列印出isa的內容是:1011101100000000000000100000000101100010101000101000001,NSObject類物件的指標是:100000000101100010101000101000000。首先將isa的內容補充至64位

0000 0101 1101 1000 0000 0000 0001 0000 0000 1011 0001 0101 0001 0100 0001
複製程式碼

取第4位到第47位之間的內容,也就是shiftcls的值:

000 0000 0000 0001 0000 0000 1011 0001 0101 0001 0100 0
複製程式碼

將類物件的指標右移三位,即去除後三位的0,得到

100000000101100010101000101000
複製程式碼

和上面的shiftcls對比:

                 10 0000 0001 0110 0010 1010 0010 1000
0000 0000 0000 0010 0000 0001 0110 0010 1010 0010 1000
複製程式碼

可以確認:shiftcls中的確包含了類物件的指標

其他位

上面已經介紹了nonpointer、magic、shiftcls、has_cxx_dtor,還有一些其他位沒有介紹,這裡簡單瞭解一下。

  1. has_assoc: 表示物件是否含有關聯引用(associatedObject)
  2. weakly_referenced: 表示物件是否含有弱引用物件
  3. deallocating: 表示物件是否正在釋放
  4. has_sidetable_rc: 表示物件的引用計數是否太大,如果太大,則需要用其他的資料結構來存
  5. extra_rc:物件的引用計數大於1,則會將引用計數的個數存到extra_rc裡面。比如物件的引用計數為5,則extra_rc的值為4。

extra_rc和has_sidetable_c可以一起理解。extra_rc用於存放引用計數的個數,extra_rc佔8位,也就是最大表示255,當物件的引用計數個數超過257時,has_sidetable_rc的值應該為1。

總結

至此,isa結構體的介紹就完了。需要提醒的是,上面的程式碼是執行在macOS上,也就是x86_64架構上的,isa結構體也是基於x86_64架構的。在arm64架構上,isa結構體中變數所佔用的位數和x86_64架構是不一樣的,但是表示的含義是一樣的。理解了x86_64架構下的isa結構體,相信對於理解arm架構下的isa結構體,應該不是什麼難事。

參考文章

從 NSObject 的初始化了解 isa

相關文章