Runtime中的 isa 結構體

原文鏈接

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

在具體了解Runtime之前,先提一個問題,什么是動態(tài)語言?

Objective-C是一門動態(tài)語言

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

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

事實上也確實如此,C語言是一門靜態(tài)語言,Objective-C是一門動態(tài)語言。然而,還是說不出靜態(tài)語言和動態(tài)語言到底有什么區(qū)別……

靜態(tài)語言和動態(tài)語言

靜態(tài)語言,可以理解成在編譯期間就確定一切的語言。以C語言來舉例,C語言編譯后會成為一個可執(zhí)行文件。假設我們在C代碼中寫了一個hello函數(shù),并且在主程序中調用了這個hello函數(shù)。倘若在編譯期間,hello函數(shù)的入口地址相對于主程序入口地址的偏移量是0x0000abcdef(不要在意這個值,只是用來舉例),那么在執(zhí)行該程序時,執(zhí)行到hello函數(shù)時,一定執(zhí)行的是相對主程序入口地址偏移量為0x0000abcdef的代碼塊。也就是說,靜態(tài)語言,在編譯期間就已經(jīng)確定一切,運行期間只是遵守編譯期確定的指令在執(zhí)行。

作為對比,再看一下動態(tài)語言,以經(jīng)常用到的Objective-C為例。假設在Objective-C中寫了hello方法,并且在主程序中調用了hello方法,也就是發(fā)送hello消息。在編譯期間,只能確定要向某個對象發(fā)送hello消息,但是具體執(zhí)行哪個內存塊的代碼是不確定的,具體執(zhí)行的代碼需要在運行期間才能確定。

到這里,靜態(tài)語言和動態(tài)語言的區(qū)別已經(jīng)很明顯了。靜態(tài)語言在編譯期間就已經(jīng)確定一切,而動態(tài)語言編譯期間只能確定一部分,還有一部分需要在運行期間才能確定。也就是說,動態(tài)語言成為一個可執(zhí)行程序并能夠正確的執(zhí)行,除了需要一個編譯器外,還需要一套運行時系統(tǒng),用于確定到底執(zhí)行哪一塊代碼。Objective-C中的運行時系統(tǒng)內就是Runtime。

Runtime源碼

Runtime源碼是一套用C語言實現(xiàn)的API,整套代碼是開源的,可以從蘋果開源網(wǎng)站上下載Runtime源碼。默認下載的Runtime源碼是不能編譯的,通過修改配置和導入必要的頭文件,可以編譯成功Runtime源碼。我在github上放了編譯成功的Runtime源碼,且有我在看Runtime源碼時的一些注釋,本篇文章中的代碼也是基于此Runtime源碼。

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

isa結構體

有經(jīng)驗的iOS開發(fā)者可能都聽過一句話:在Objective-C語言中,類也是對象,且每個對象都包含一個isa指針,isa指針指向該對象所屬的類。不過現(xiàn)在Runtime中的對象定義已經(jīng)不是這樣了,現(xiàn)在使用的是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具體是什么,后續(xù)再介紹。

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能夠使該對象找到自己所屬的類。為什么對象需要知道自己所屬的類呢?這主要是因為對象的方法是存儲在該對象所屬的類中的。

這一點是很容易理解的,一個類可以有多個對象,倘若每個對象都含有自己能夠執(zhí)行的方法,那對于內存來說是災難級的。

在向對象發(fā)送消息,也就是實例方法被調用時,對象通過自己的isa找到所屬的類,然后在類的結構中找到對應方法的實現(xiàn)(關于在類結構中如何找到方法的實現(xiàn),后續(xù)的文章再介紹)。

我們知道,Objective-C中區(qū)分類方法和實例方法。實例方法是如何找到的我們了解了,那么類方法是如何找到的呢?類結構體中也有isa,類對象的isa指向哪里呢?

元類(metaClass)

為了解決類方法調用,Objective-C引入了元類(metaClass),類對象的isa指向該類的元類,一個類對象對應一個元類對象。

元類對象也是類對象,既然是類對象,那么元類對象中也有isa,那么元類的isa又指向哪里呢?總不能指向元元類吧……這樣是無窮無盡的。

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

image

isa結構體定義

了解了isa的作用,現(xiàn)在來看一下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架構下有區(qū)別,但是不影響我們理解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是聯(lián)合體,也就是說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結構體中,看一下官方提供的函數(shù)和注釋:

// 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,協(xié)議對象初始化isa,實例對象初始化isa,其他對象初始化isa,分別對應不同的函數(shù)。

看下每個函數(shù)的實現(xiàn):

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);
}

可以看到,無論是類對象,實例對象,協(xié)議對象,還是其他對象,初始化isa結構體最終都調用了

inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)

函數(shù),只是所傳的參數(shù)不同而已。

最終調用的initIsa函數(shù)的代碼,經(jīng)過簡化后如下:

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位
        // 根據(jù)注釋,使用ISA_MAGIC_VALUE賦值,實際上只是賦值了isa.magic和isa.nonpointer
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        // 將當前對象的類指針賦值到shiftcls
        // 類的指針是按照字節(jié)(8bits)對齊的,其指針后三位都是沒有意義的0,因此可以右移3位
        newisa.shiftcls = (uintptr_t)cls >> 3;
        // 賦值??醋⑨屵@個地方不是線程安全的??
        isa = newisa;
    }
}

初始化實例對象的isa時,傳入的nonpointer參數(shù)是true,所以直接走了else分之。在else分之中,對isa的bits分之賦值ISA_MAGIC_VALUE。根據(jù)注釋,這樣代碼實際上只是對isa中的magic和nonpointer進行了賦值,來看一下為什么。

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

image

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

nonpointer

在文章開頭也提到了,在Objective-C語言中,類也是對象,且每個對象都包含一個isa指針,現(xiàn)在改為了isa結構體。nonpointer作用就是區(qū)分這兩者。

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

magic的值調試器會用到,調試器根據(jù)magci的值判斷當前對象已經(jīng)初始過了,還是尚未初始化的空間。

has_cxx_dtor

接下來就是對has_cxx_dtor進行賦值。has_cxx_dtor表示當前對象是否有C++的析構函數(shù)(destructor),如果沒有,釋放時會快速的釋放內存。

shiftcls

在函數(shù)

inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 

中,參數(shù)cls就是類的指針。而

newisa.shiftcls = (uintptr_t)cls >> 3;

shiftcls存儲的到底是什么呢?

實際上,shiftcls存儲的就是當前對象類的指針。之所以右移三位是出于節(jié)省空間上的考慮。

在Objective-C中,類的指針是按照字節(jié)(8 bits)對齊的,也就是說類指針地址轉化成十進制后,都是8的倍數(shù),也就是說,類指針地址轉化成二進制后,后三位都是0。既然是沒有意義的0,那么在存儲時就可以省略,用節(jié)省下來的空間存儲一些其他信息。

在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)

函數(shù),類初始化時會調用該函數(shù)??梢栽谠摵瘮?shù)中打印類對象的地址

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,說明類對象確實是按照字節(jié)對齊,后三位都是0。因此在賦值shiftcls時,右移三位是安全的,不會丟失類指針信息。

我們可以寫代碼驗證一下對象的isa和類對象指針的關系。代碼如下:

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

// 把一個十進制的數(shù)轉為二進制
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中的確包含了類對象的指針。

其他位

上面已經(jīng)介紹了nonpointer、magic、shiftcls、has_cxx_dtor,還有一些其他位沒有介紹,這里簡單了解一下。

  1. has_assoc: 表示對象是否含有關聯(lián)引用(associatedObject)
  2. weakly_referenced: 表示對象是否含有弱引用對象
  3. deallocating: 表示對象是否正在釋放
  4. has_sidetable_rc: 表示對象的引用計數(shù)是否太大,如果太大,則需要用其他的數(shù)據(jù)結構來存
  5. extra_rc:對象的引用計數(shù)大于1,則會將引用計數(shù)的個數(shù)存到extra_rc里面。比如對象的引用計數(shù)為5,則extra_rc的值為4。

extra_rc和has_sidetable_c可以一起理解。extra_rc用于存放引用計數(shù)的個數(shù),extra_rc占8位,也就是最大表示255,當對象的引用計數(shù)個數(shù)超過257時,has_sidetable_rc的值應該為1。

總結

至此,isa結構體的介紹就完了。需要提醒的是,上面的代碼是運行在macOS上,也就是x86_64架構上的,isa結構體也是基于x86_64架構的。在arm64架構上,isa結構體中變量所占用的位數(shù)和x86_64架構是不一樣的,但是表示的含義是一樣的。理解了x86_64架構下的isa結構體,相信對于理解arm架構下的isa結構體,應該不是什么難事。

參考文章

從 NSObject 的初始化了解 isa

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

相關閱讀更多精彩內容

  • 轉至元數(shù)據(jù)結尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,030評論 0 9
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡介 Runt...
    樂樂的簡書閱讀 2,240評論 0 9
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,621評論 1 32
  • RunTime:指一個程序在運行(或者在被執(zhí)行)的狀態(tài)。也就是說,當你打開一個程序使它在電腦上運行的時候,那個程序...
    悟2023閱讀 511評論 0 1
  • 本文基于objc4-709源碼進行分析。關于源碼編譯:objc - 編譯Runtime源碼objc4-706 ob...
    WeiHing閱讀 1,044評論 1 3

友情鏈接更多精彩內容