objc源碼解析-ObjectiveC對象結構

概要

本文將從源碼角度分析 Objective-C 對象的數據結構,閱讀本文須要對 Objective-C 語言有基本瞭解。本文的源碼來自objc4-706,可在該頁面下載源碼。另附一份可運行的Runtime源碼。html

1、Objective-C 對象定義

Objective-C 是一種面向對象的語言,NSObject 是全部類的基類。咱們能夠打開 NSObject.h 文件查看到 NSObject 的類定義以下:ios

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
複製代碼

這裏表示一個 NSObject 擁有一個 Class 類型的成員變量,那麼這個 Class 是什麼意思?咱們能夠在 objc4-706 源碼的 objc-private.h 中看到以下兩個定義:git

typedef struct objc_class *Class;
typedef struct objc_object *id;
複製代碼

從第一個定義中能夠看出,Class 其實就是 C 語言定義的結構體類型(struct objc_class)的指針,這個聲明說明 Objective-C 的類實際上就是 struct objc_class。github

第二個定義中出現了咱們常常遇到的 id 類型,這裏能夠看出 id 類型是 C 語言定義的結構體類型(struct objc_object)的指針,咱們知道咱們能夠用 id 來聲明一個對象,因此這也說明了 Objective-C 的對象實際上就是 struct objc_object。objective-c

在 objc4-680 源碼中咱們跳轉到 objc_class 的定義:macos

// note:這裏沒有列出結構體中定義的方法
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 的,因此 Objective-C 中的類自身也是一個對象,只是除了 objc_object 中定義的成員變量外,還有另外三個成員變量:superclass、cache 和 bits。segmentfault

因此,Objective-C 中最基本的數據結構就是:struct objc_object,objc_object 結構體定義以下:緩存

// note:這裏沒有列出結構體中定義的方法
struct objc_object {
private:
    isa_t isa;
};

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

    Class cls;
    uintptr_t bits;

#if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
#elif __x86_64__
    // Definition for x86_64, not listed, check source code in objc-private.h.
# else
#   error unknown architecture for packed isa
#endif
複製代碼

Note: 上面對源碼進行了簡化,源碼中有多個條件編譯指令。爲了解除閱讀源碼時對上述簡化代碼的疑惑,這裏簡單介紹下源碼中幾個條件編譯宏(若是未閱讀源碼,可沒必要關心下面的解釋):數據結構

  1. SUPPORT_PACKED_ISA:表示平臺是否支持在 isa 指針中插入除 Class 以外的信息。若是支持就會將 Class 信息放入 isa_t 定義的 struct 內,並附上一些其餘信息,例如上面的 nonpointer 等等;若是不支持,那麼不會使用 isa_t 內定義的 struct,這時 isa_t 只使用 cls(Class 指針)。在 iOS 以及 MacOSX 上,SUPPORT_PACKED_ISA 定義爲 1。
  2. __arm64__、__x86_64__ 表示 CPU 架構,例如電腦通常是 __x86_64__ 架構,手機通常是 arm 結構,這裏 64 表明是 64 位 CPU。上面只列出了 __arm64__ 架構的定義。
  3. 對於 SUPPORT_PACKED_ISA(見第一點)的 isa 指針,SUPPORT_INDEXED_ISA 表示 isa_t 中存放的 Class 信息是 Class 的地址,仍是一個索引(根據該索引可在類信息表中查找該類結構地址)。經測試,iOS 設備上 SUPPORT_INDEXED_ISA 是 0。
  4. 除了上述的條件編譯宏,這裏提一下 Union 結構,若是對 C 語言 union 不瞭解的能夠參考 C/C++中 union 用法總結Why we need C Unions?

本節經過源碼查看了 Objective-C 語言中的類、對象以及相關數據結構的定義,能夠看出 isa_t 結構很是關鍵,下面來分析一下 isa_t 結構。架構

2、深刻理解 isa_t

上面已經給出了 isa_t 在 __arm64__ 架構下的定義,這裏繼續以 __arm64__ 架構下的定義分析(__x86_64__架構下很是相似)。下圖給出了 __arm64__ 架構下 isa_t 結構的內存佈局:

Screen Shot 2017-03-23 at 21.58.11.png

isa_t 結構中的 struct 的成員變量都繪製在了上圖中,下面逐個分析各個字段的含義:

nonpointer: 表示是否對 isa 指針開啓指針優化

在說明 nonpointer 意義前,先簡單介紹一下蘋果爲 64 位設備提出的節省內存和提升執行效率的一種優化方案:Tagged Pointer。

設想在 32 位和 64 位設備上分別存儲一個 NSNumber 對象,其值是一個 NSInteger 整數。

首先,分析一下內存佔用狀況:

  1. 讀寫 NSNumber 對象的指針。在 32 位設備上,一個指針須要 4byte。在 64 位設備上,一個指針須要 8byte。

  2. 存儲 NSNumber 對象值的內存。在 32 位設備上,NSInteger 佔用 4byte。在 64 位設備上,NSInteger 佔用 8byte。

  3. Objective-C 內存管理採用引用計數的方式,咱們須要使用額外的空間來存儲引用計數,若是引用計數使用 NSInteger,那麼 64 位設備會比 32 位設備多用 4byte。

此外,從效率上講,引用計數、生命週期標識等存儲在其餘地方,也有很多處理邏輯(例如爲引用計數動態分配內存等)。

通常來講,32 位已經足夠存儲咱們一般遇到的整數和指針地址了,那麼在 64 位設備上,就有 32 位地址空間浪費掉了,存儲一個值爲 NSInteger 的 NSNumber 對象就浪費了 8byte 的空間(4byte 指針和 4byte value)。

爲了節省內存以及提升程序執行效率,蘋果提出了 Tagged Pointer,Tagged Pointer 簡單來講就是使用存儲指針的內存空間存儲實際的數據。

例如,NSNumber 指針在 64 位設備上佔用 8byte 內存空間,指針優化能夠將 NSNumber 的值經過某種規則放入到存儲 NSNumber 指針地址的 8byte 中,這樣就減小了 NSInteger 所需的 8byte 內存空間,從而節省了內存。

另外,Tagged Pointer 已經再也不是對象指針,它裏面存放着實際數據,只是一個普通變量,因此它的內存無需在堆上 calloc/free,從而提升了內存讀取效率。可是因爲 Tagged Pointer 不是合法的對象指針,因此咱們沒法經過 Tagged Pointer 獲取 isa 信息。關於 Tagged Pointer 更詳細的介紹可參考:深刻理解 Tagged Pointer-唐巧,這裏不作深刻介紹。

瞭解 Tagged Pointer 的概念後,再來看 nonpointer 變量。nonpointer 變量佔用 1bit 內存空間,能夠有兩個值:0 和 1,分別表明不一樣的 isa_t 的類型:

  1. 0 表示 isa_t 沒有開啓指針優化,不使用 isa_t 中定義的結構體。訪問 objc_object 的 isa 會直接返回 isa_t 結構中的 cls 變量,cls 變量會指向對象所屬的類的結構,在 64 位設備上會佔用 8byte。

  2. 1 表示 isa_t 開啓了指針優化,不能直接訪問 objc_object 的 isa 成員變量(由於 isa 已經不是一個合法的內存指針了,見 Tagged Pointer 的介紹),從其名字 nonpointer 也可獲知這個 isa 已經不是一個指針了。可是 isa 中包含了類信息、對象的引用計數等信息,在 64 位設備上充分利用了內存空間。

對於 nonpointer 爲 1 的 isa_t 的結構就是 isa_t 內部定義的結構體,該結構體中包含了對象的所屬類信息、引用計數等。從這裏也能夠看出,指針優化減小了內存使用,而且引用計數等對象關聯信息都存放在 isa_t 中,也減小了不少獲取對象信息的邏輯,提升了執行效率。

shiftcls:

存儲類指針的值。開啓指針優化的狀況下,在 arm64 架構中有 33 位用來存儲類指針。

其他變量

其餘幾個變量很容易理解,這裏再也不作太多介紹。

  1. has_assoc 該變量與對象的關聯引用有關,當對象有關聯引用時,釋放對象時須要作額外的邏輯。關聯引用就是咱們一般用 objc_setAssociatedObject 方法設置給對象的,這裏對於關聯引用不作過多分析,若是後續有時間寫關聯引用實現時再深刻分析關聯引用有關的代碼。

  2. has_cxx_dtor 表示該對象是否有 C++ 或者 Objc 的析構器,若是有析構函數,則須要作析構邏輯,若是沒有,則能夠更快的釋放對象。

  3. magic 用於判斷對象是否已經完成了初始化,在 arm64 中 0x16 是調試器判斷當前對象是真的對象仍是沒有初始化的空間(在 x86_64 中該值爲 0x3b)。

  4. weakly_referenced 標誌對象是否被指向或者曾經指向一個 ARC 的弱變量,沒有弱引用的對象能夠更快釋放。

  5. deallocating 標誌對象是否正在釋放內存。

  6. extra_rc 表示該對象的引用計數值,其實是引用計數值減 1,例如,若是對象的引用計數爲 10,那麼 extra_rc 爲 9。若是引用計數大於 10,則須要使用到下面的 has_sidetable_rc。

  7. has_sidetable_rc 當對象引用技術大於 10 時,則須要借用該變量存儲進位(相似於加減法運算中的進位借位)。

  8. ISA_MAGIC_MASK 經過掩碼方式獲取 magic 值。

  9. ISA_MASK 經過掩碼方式獲取 isa 的類指針值。

  10. RC_ONE 和 RC_HALF 用於引用計數的相關計算。

struct objc_object 中的方法

經過源碼查看 struct objc_object,咱們能夠看到其中定義了不少方法。例如 isa_t 中的與類指針相關的兩個方法:

Class ISA();
Class getIsa();
複製代碼

爲何須要定義方法來操做 isa 指針?這裏只由於對 isa_t 的變量操做封裝了方法是由於前面介紹了開啓了指針優化的 isa 已經不是一個合法的指針了,咱們沒法直接操做對象的 isa 指針,只有經過方法來進行相應操做。

3、對象、類、元類

第一部分討論了 NSObject、objc_object、objc_class 的定義,能夠看出 Class 其實就是 C 語言定義的 objc_class 結構體,而 objc_class 繼承自 objc_object,因此 Objective-C 中 Class 也是一個對象。第二部分深刻解析了 objc_object 中惟一的成員變量的類型:isa_t,這部分討論了 isa_t 中存放着對象所屬的類的指針。第三部分咱們來討論在 Objective-C 的對象、類和元類(meta-class,後面會介紹)的關係。

剛剛提到,objc_class 繼承自 objc_object,因此 objc_class 也是有 isa_t 類型的 isa 成員變量的,那麼 objc_class 的 isa_t 中的 shiftcls 表示了什麼意思呢?這裏引入一個新的概念:元類。objc_class 的 isa_t 中的 shiftcls 就指向了 objc_object 的元類。什麼是元類?

先來看一下 objc_class 除了繼承自 objc_object 的成員變量 isa_t 外的三個成員變量:

  1. Class superclass:該變量指向父類的 objc_class;
  2. cache_t cache:該變量存放着實例方法的緩存,爲了提升每次執行;
  3. class_data_bits_t bits:存放着實例的全部方法。

關於 Objective-C 的 Runtime 的方法查找這裏不進深刻討論。不過,經過上面三個屬性的解釋,咱們能夠窺探出一個對象能夠調用的方法列表是存儲在對象的類結構中的。其實不存儲在對象中的緣由也很好理解,若是方法列表存放在對象結構中,那每建立一個對象,就要增長一個實例方法列表,資源消耗過大,因此存儲在了類結構中。可是,除了實例方法外,通常還會法就存放在了上面提到的元類裏。元類也是一個 objc_class 結構,結構中有 isa 和 superclass 指針,下圖是 Objective-C Runtime 講解中最經典的一張圖:

meta_class.png

注意:上圖中 isa 的箭頭,其實並非 isa 指針直接指向了相應結構,而是 isa_t 中的 shiftcls 指向了相應結構。

這裏根據上述分析以及上圖給出幾個總結點:

  1. 每一個類都有其對應的元類;

  2. 一個類的類結構中存儲着該類全部實例方法,對象經過 isa 去類結構中獲取實例方法實現,若該類結構中沒有所需的實例方法,則經過 superclass 指針去父類結構查找,直到 Root class(class)。

  3. 一個類的元類中存儲着該類全部類方法,類對象經過 isa 去元類結構中獲取類方法實現,若元類結構中沒有所需的類方法,則經過 superclass 指針去父類元類結構查找,直到 Root class(class)。

  4. 在 Objective-C 中,Root class(class)其實就是 NSObject,NSObject 的 superclass 指向 nil。

  5. 在 Objective-C 中,全部的對象(包含 instance、class、meta-class)均可以調用 NSObject 的實例方法。

  6. 在 Objective-C 中,全部的 class 以及 meta-class 均可以調用 NSObject 的類方法。

若是對於元類(meta-class)還不理解,推薦閱讀 what is meta class in objective-c?,此文對 meta-class 的解釋通俗易懂,建議閱讀。

4、對象初始化過程

第四部分將從源碼角度解析一個 NSObject 對象建立的過程。

咱們知道建立一個 NSObject 對象的代碼爲:[[NSObject alloc] init];(還有一種方式是使用 [NSObject new],查看源碼能夠看到內部其實與第一種方式徹底相同)。

+ (id)alloc {
    return _objc_rootAlloc(self);
}

_objc_rootAlloc(Class cls) {
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {
    // means if (checkNil && !cls)) return nil;
    // besides, if checkNil && !cls probably to be false, "return nil" is optimized.
    if (slowpath(checkNil && !cls)) return nil; // 檢查 cls 信息是否爲 nil,若是爲 nil,則沒法建立新對象,返回 nil。

#if __OBJC2__ // If Objective-C 2.0 or later.
    if (fastpath(!cls->ISA()->hasCustomAWZ())) { // 檢查類是否有默認的 alloc/allocWithZone 實現
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) { // 是否能夠快速分配內存(這裏跟蹤源碼可看到返回了 false)
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0); // 這裏是建立對象的關鍵函數調用
            if (slowpath(!obj)) return callBadAllocHandler(cls); // 檢查新建的對象是否合法
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil]; // 這裏 cls 的 allocWithZone 方法裏也是調用了 class_createInstance。
    return [cls alloc];
}
複製代碼

能夠看到,新建一個對象主要是 callAlloc 函數。在該函數中有一個 slowpath,咱們來看下定義:

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
複製代碼

其中 __builtin_expect(EXP, N) 表示 EXP == N 編譯器優化的 gcc 內建函數。經過這種方式,編譯器在編譯過程當中會把可能性更大的 if 分支代碼緊跟前面的代碼,從而減小指令跳轉帶來的性能的降低。因此從邏輯上講 if(slowpath(x)) 與 if(x) 的含義相同,只不過是多了編譯器優化的內容。

上述代碼中已經對一些語句進行了註釋,該方法中大部分語句都是處理新建對象使用的 zone。咱們這裏重點分析 class_createInstance 函數,下面是源碼中 class_createInstance 的實現:

id class_createInstance(Class cls, size_t extraBytes) {
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

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; // 檢查 cls 是否合法

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor(); // 是否有構造函數
    bool hasCxxDtor = cls->hasCxxDtor(); // 是否有析構函數
    bool fast = cls->canAllocNonpointer(); // 是否使用原始 isa 格式(見第二部分對 isa 的介紹)

    size_t size = cls->instanceSize(extraBytes); // 須要分配的空間大小,打開 instanceSize 實現能夠知道對象是按照 16bytes 對齊的
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor); // 這一步是關鍵,用於初始化建立的內存空間
    }
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}
複製代碼

上述函數實現中,也有了關鍵位置的註釋,這裏咱們直接分析最重要部分: obj->initInstanceIsa(cls, hasCxxDtor); 該部分代碼能夠根據第二部分進行簡化,簡化後以下:

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

inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) {
    assert(!isTaggedPointer());
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
    }
}
複製代碼

上述代碼中,newisa.bits = ISA_MAGIC_VALUE; 是爲了對 isa 結構賦值一個初始值,ISA_MAGIC_VALUE 的值爲 0x001d800000000001ULL,經過第二部分對 isa_t 的結構分析,咱們能夠知道這次賦值只是對 nonpointer 和 magic 部分進行了賦值。

newisa.shiftcls = (uintptr_t)cls >> 3; 是將類的地址存儲在對象的 isa 結構中, 這裏右移三位的主要緣由是用於將 Class 指針中無用的後三位清除減少內存的消耗,由於類的指針要按照字節(8 bits)對齊內存,其指針後三位都是沒有意義的 0。關於類指針對齊的詳細解析可參考:從 NSObject 的初始化了解 isa

初始化 isa 以後,[NSObject alloc] 的工做算是作完了,下面就是 init 相關邏輯:

- (id)init {
    return _objc_rootInit(self);
}

id _objc_rootInit(id obj) {
    return obj;
}
複製代碼

能夠看到 init 其實只是返回了新建的對象指針,沒有其餘多餘邏輯。

到這裏新建一個對象的全部邏輯就結束了。

5、參考資料

1. 神經病院 Objective-C Runtime 入院第一天— isa 和 Class

2.從 NSObject 的初始化了解 isa

3.深刻理解 Tagged Pointer

4.ObjC runtime 源碼 閱讀筆記(一)

5.What is a meta-class in Objective-C?

Note: 文中內容不表明權威,有任何問題均可以進行交流。轉載請註明原文地址。

相關文章
相關標籤/搜索