OC源碼分析之isa

前言

想要成爲一名iOS開發高手,免不了閱讀源碼。如下是筆者在OC源碼探索中梳理的一個小系列——類與對象篇,歡迎你們閱讀指正,同時也但願對你們有所幫助。程序員

  1. OC源碼分析之對象的建立
  2. OC源碼分析之isa
  3. 未完待續...

1. isa介紹

1.1 isa是什麼

OC源碼分析之對象的建立 一文中,咱們知道alloc底層會調用calloc分配內存,接着就是initInstanceIsa(cls, hasCxxDtor),顧名思義是初始化對象的isa,其關鍵代碼以下數據結構

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

    // 留意這裏的true
    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);

#if SUPPORT_INDEXED_ISA
        assert(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif

        // This write must be performed in a single store in some cases
        // (for example when realizing a class because other threads
        // may simultaneously try to use the class).
        // fixme use atomics here to guarantee single-store and to
        // guarantee memory order w.r.t. the class index table
        // ...but not too atomic because we don't want to hurt instantiation
        isa = newisa;
    }
}

複製代碼
  1. 關於Tagged Pointer

聽說,爲了節省內存和提升執行效率,蘋果提出了Tagged Pointer的概念。對於 64 位程序,引入Tagged Pointer後,相關邏輯能減小一半的內存佔用,以及 3倍 的訪問速度提高,100倍 的建立、銷燬速度提高。架構

Tagged Pointer首次應用於iPhone 5s設備上,如今幾乎都應用Tagged Pointer了。想了解更多關於Tagged Pointer的內容可戳 深刻理解 Tagged Pointeride

  1. isa_t類型

點擊objc_object::initIsa()中的isa,發現isaisa_t類型函數

struct objc_object {
private:
    isa_t isa;
    
    ... // 一些isa的公有、私有方法
};
複製代碼

isa_t其實是一個union(即聯合體,也叫共用體)源碼分析

這裏先普及一下structunion的區別post

  1. 二者均可以包含多個不一樣類型的數據,如intdoubleClass等。
  2. struct中各成員有各自的內存空間,一個struct變量的內存總長度大於等於各成員內存長度之和;而在union中,各成員共享一段內存空間,一個union變量的內存總長度等於各成員中內存最長的那個成員的內存長度。
  3. struct中的成員進行賦值,不會影響其餘成員的值;對union中的成員賦值時,每次只能給一個成員賦值,同時其它成員的值也就不存在了。

isa_t包含了clsbits兩個成員變量,其結構以下性能

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

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
複製代碼

1.2 isabits成員變量

  1. 位域

這裏普及一下位域的概念優化

位域是一種數據結構,能夠把數據以位的形式緊湊的儲存,並容許程序員對此結構的位進行操做。ui

  • 優勢:
    • 節省儲存空間;
    • 能夠很方便的訪問一個整數值的部份內容從而能夠簡化程序源代碼。
  • 缺點:
    • 其內存分配與內存對齊的實現方式依賴於具體的機器和系統,在不一樣的平臺可能有不一樣的結果,這致使了位段在本質上是不可移植的。

isabits成員變量類型是uintptr_t,它實質上是個unsigned long

typedef unsigned long           uintptr_t;
複製代碼

64位CPU架構下bits長度爲64位,也就是8字節,其各個位的存儲就使用了位域,即ISA_BITFIELD

  1. ISA_BITFIELD

接下來看一下ISA_BITFIELD的源碼(因爲筆者是用macOS項目研究OC底層源碼,因此這裏以x86_64架構爲例)

# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \ 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
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
複製代碼

首先明確一點,在64位CPU架構下isa指針的長度也是8字節,它能夠存儲足夠多的內容,蘋果爲了優化性能,存儲類地址只用了一部分位(x86_64下是44位,arm64下是33位),剩下的位用來存儲一些其它信息。

具體分析一下ISA_BITFIELD位域各成員的表示意義:

  • nonpointer:表示是否對 isa指針 開啓指針優化。
    • 0:不優化,是純isa指針,當訪問isa指針時,直接返回其成員變量cls
    • 1:優化,即isa 指針內容不止是類地址,還包含了類的一些信息、對象的引用計數等。
  • has_assoc:是否有關聯對象。
  • has_cxx_dtor:該對象是否有C++或Objc的析構器。
    • 若是有析構函數,則須要作一些析構的邏輯處理;
    • 若是沒有,則能夠更快的釋放對象。
  • shiftcls:存儲類指針的值。開啓指針優化的狀況下,在 x86_64 架構有 44位 用來存儲類指針,arm64 架構中有 33位
  • magic:用於調試器判斷當前對象是真的對象,仍是一段沒有初始化的空間。
  • weakly_referenced:用於標識對象是否被指向或者曾經被指向一個ARC的弱變量,沒有弱引用的對象釋放的更快。
  • deallocating:標識對象是否正在釋放內存。
  • has_sidetable_rc:對象的引用計數值是否有進位。
  • extra_rc:表示該對象的引用計數值。extra_rc只是存儲了額外的引用計數,實際的引用計數公式:實際引用計數 = extra_rc + 1。這裏佔了8位,因此理論上能夠存儲的最大引用計數是:2^8 - 1 + 1 = 256arm64CPU架構下的extra_rc佔19位,可存儲的最大引用計數爲2^19 - 1 + 1 = 524288)。
    • has_sidetable_rc的關聯:當對象的最大引用計數超過界限後,has_sidetable_rc的值爲1,不然爲0

1.3 isacls成員變量

分析完isa的位域,接下來就只剩下cls,它是Class類型,一樣上源碼

typedef struct objc_class *Class;
// 順便了解一下id的類型,顯然id是個指針變量,它的值只有一個isa變量
typedef struct objc_object *id;

struct objc_class : objc_object {
    // Class ISA; 
    Class superclass;
    cache_t cache;             
    class_data_bits_t bits;

    class_rw_t *data() { 
        return bits.data();
    }
    ... // 一些方法
};

struct objc_object {
private:
    isa_t isa;
    
    ... // 一些isa的公有、私有方法
};

複製代碼

從源碼得知,Class其實是objc_class結構體的指針變量,而objc_class又繼承自objc_object,所以Class這個結構體指針變量的值內部有一個isa成員變量(類型爲isa_t),這個isa成員變量在64位CPU架構下是8字節,且排在objc_class結構體的前8字節。

1.4 isa的做用

經過對isa的位域說明,咱們知道shiftcls存儲的是類指針的值。在x86_64架構下,shiftcls佔用44位,也就是第[3, 46]位。將 [3, 46]位 所有填充1,[0, 2]位 和 [47~63]位 都補0,獲得0x7ffffffffff8,也就是ISA_MASK的值。故,isa & ISA_MASK會獲得shiftcls存儲的類指針的值。這也就是所謂MASK的做用。

以下圖所示

下面用一個例子說明isa的做用

此時經過lldb命令調試

說明:

  1. 0x001d800100001129對象pisa值,經過isa & ISA_MASK運算獲得的0x0000000100001128就是Person類的地址
  2. 證實【1】:經過p/x Person.class直接打印Person類地址,顯然獲得的是0x0000000100001128,如此【1】證實成立!

結論:isa將對象和類關聯起來,起到了中間橋樑的做用。

思考:若是不用ISA_MASK,那麼如何證實isa的這個做用呢?——答案將在文末補充。

1.5 isa的初始化補充

最後補充一下isa的初始化。還記得初始化isa的入口嗎?是initIsa(cls, true, hasCxxDtor);,此時nonpointer的值是true,再看SUPPORT_INDEXED_ISA的定義

#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
複製代碼

x86_64下,SUPPORT_INDEXED_ISA是0,因此isa的初始化最終會來到

isa_t newisa(0);

// 使用ISA_MAGIC_VALUE(0x001d800000000001ULL)賦值給bits
// nonpointer爲1,magic爲1d,其餘變量爲零
newisa.bits = ISA_MAGIC_VALUE;
// hasCxxDtor是從類的isa中取出的
newisa.has_cxx_dtor = hasCxxDtor;
// 將cls右移3位後賦值給shiftcls
newisa.shiftcls = (uintptr_t)cls >> 3;

isa = newisa;
複製代碼

礙於篇幅,這裏不繼續深刻hasCxxDtor

2 isa指向圖

經過以上的源碼分析,咱們認識到對象的isa指針指向了對象所屬的類。而類自己也有一個isa指針,它指向的又是什麼呢?

蘋果官方有個isa指向圖,即

從圖可知,類的isa指針指向的是類的元類。如今咱們來驗證一下吧。

2.1 準備工做

建立Teacher類、Person類,其中Person類繼承於NSObjectTeacher類繼承於Person類。

對比isa指向圖,對號入座後就是,Teacher類至關於SubclassPerson類至關於SuperclassNSObject至關於Root class

2.2 驗證過程

  1. 獲取teacher對象的類(結果是Teacher類,地址爲0x0000000100001230
(lldb) x/4gx teacher
0x100f59400: 0x001d800100001231 0x0000000000000000
0x100f59410: 0x636f72504b575b2d 0x70756f7247737365
(lldb) p/x 0x001d800100001231 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $3 = 0x0000000100001230      // 對象teacher的類地址
(lldb) po $3
Teacher     // 對象teacher的類
複製代碼
  1. 獲取Teacher類的元類(結果是Teacher元類,地址爲0x0000000100001208
(lldb) x/4gx Teacher.class
0x100001230: 0x001d800100001209 0x00000001000011e0
0x100001240: 0x0000000100f61150 0x0000000100000003
(lldb) p/x 0x001d800100001209 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $5 = 0x0000000100001208      // Teacher類的元類地址
(lldb) po $5
Teacher     // Teacher類的元類
複製代碼

Teacher類 和 Teacher元類 地址不同

  1. 獲取person對象的類(結果是Person類,地址爲0x00000001000011e0),以及類的元類(結果是Person元類,地址爲0x00000001000011b8
(lldb) x/4gx person
0x100f60a30: 0x001d8001000011e1 0x0000000000000000
0x100f60a40: 0x0000000000000002 0x00007fff9b855588
(lldb) p/x 0x001d8001000011e1 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $8 = 0x00000001000011e0      // 對象person的類地址
(lldb) po $8
Person      // 對象person的類

(lldb) x/4gx Person.class
0x1000011e0: 0x001d8001000011b9 0x0000000100b38140
0x1000011f0: 0x0000000100f61030 0x0000000100000003
(lldb) p/x 0x001d8001000011b9 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $10 = 0x00000001000011b8     // Person類的元類地址
(lldb) po $10
Person      // Person類的元類
複製代碼
  1. 獲取object對象的類(結果是NSObject類,地址爲0x0000000100b38140),以及類的元類(結果是NSObject元類,地址爲0x0000000100b380f0
(lldb) x/4gx object
0x100f5cc50: 0x001d800100b38141 0x0000000000000000
0x100f5cc60: 0x70736e494b575b2d 0x574b57726f746365
(lldb) p/x 0x001d800100b38141 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $12 = 0x0000000100b38140     // 對象object的類地址
(lldb) po $12   
NSObject    // 對象object的類

(lldb) x/4gx NSObject.class
0x100b38140: 0x001d800100b380f1 0x0000000000000000
0x100b38150: 0x0000000101913060 0x0000000200000003
(lldb) p/x 0x001d800100b380f1 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $14 = 0x0000000100b380f0     // NSObject類的元類地址
(lldb) po $14
NSObject    // NSObject類的元類
複製代碼
  1. 獲取Teacher元類的元類,Person元類的元類,以及NSObject元類的元類
(lldb) x/4gx 0x0000000100001208     // Teacher元類
0x100001208: 0x001d800100b380f1 0x00000001000011b8
0x100001218: 0x000000010186f950 0x0000000400000007
(lldb) p/x 0x001d800100b380f1 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $16 = 0x0000000100b380f0     // NSObject元類
(lldb) po $16
NSObject    // NSObject元類

(lldb) x/4gx 0x00000001000011b8     // Person元類
0x1000011b8: 0x001d800100b380f1 0x0000000100b380f0
0x1000011c8: 0x0000000101905a50 0x0000000300000007
(lldb) p/x 0x001d800100b380f1 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $17 = 0x0000000100b380f0     // NSObject元類
(lldb) po $17
NSObject    // NSObject元類

(lldb) x/4gx 0x0000000100b380f0     // NSObject元類
0x100b380f0: 0x001d800100b380f1 0x0000000100b38140
0x100b38100: 0x0000000101903820 0x0000000500000007
(lldb) p/x 0x001d800100b380f1 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $18 = 0x0000000100b380f0     // NSObject元類
(lldb) po $18
NSObject    // NSObject元類
複製代碼

2.3 isa指向結論

基於【2.2】的驗證過程,能夠得出結論:

  • 對象的isa指針 指向 對象的所屬類(如person對象的isa指向Person類)
  • 類的isa指針 指向 類的元類(如Person類的isa指向Person元類
  • 元類的isa指針 指向 根元類(如Person元類isa指向NSObject元類
    • NSObject類的元類是根元類
    • NSObject元類isa指針 指向自身(是個圓圈)

思考:若是Person類繼承的是NSProxy,相關isa指向是怎樣的呢?感興趣的能夠去試試。

2.4 繼承關係的證實

類的繼承關係證實過程:(以 -> 表示 繼承自)

(lldb) p class_getSuperclass(Teacher.class)
(Class) $19 = Person    // Teacher類 -> Person類

(lldb) p class_getSuperclass(Person.class)
(Class) $20 = NSObject  // Person類 -> NSObject類

(lldb) p class_getSuperclass(NSObject.class)
(Class) $21 = nil       // NSObject類 -> nil
複製代碼

元類的繼承關係證實過程:(以 -> 表示 繼承自)

// 0x0000000100001208 是 Teacher元類
(lldb) p/x class_getSuperclass((Class)0x0000000100001208)
(Class) $17 = 0x00000001000011b8    // Person元類
(lldb) po $17
Person      // Teacher元類 -> Person元類

// 0x00000001000011b8 是 Person元類
(lldb) p/x class_getSuperclass((Class)0x00000001000011b8)
(Class) $22 = 0x0000000100b380f0    // NSObject元類(根元類)
(lldb) po $22
NSObject    // Person元類 -> 根元類

// 0x0000000100b380f0 是 根元類
(lldb) p/x class_getSuperclass((Class)0x0000000100b380f0)
(Class) $23 = 0x0000000100b38140 NSObject   // NSObject類(根類)
(lldb) po $23
NSObject    // 根元類 -> 根類
複製代碼

根元類繼承自根類(NSObject元類 -> NSObject類),根類繼承自nil(NSObject類 -> nil)

2.5 isa指向圖塗鴉版

把上面的例子塗在isa指向圖上,就獲得了下圖

3. 總結

  1. isaisa_t結構,採用 聯合體+位域 的搭配來設計:在不一樣的位上顯示不一樣的內容,以此來節省儲存空間,進而優化內存。
  2. isa包含了clsbits兩個成員變量,這兩個成員變量在64位CPU架構下的長度都是8字節,因此isa64位CPU架構下的長度也是8字節。
  3. isa的位域上存儲了一些對象與類的信息,並將對象與類關聯起來,起到中間橋樑的做用。
  4. isa指向圖相關結論:
    • 對象的isa指針 指向 對象的所屬類(如person對象的isa指向Person類)
    • 類的isa指針 指向 類的元類(如Person類的isa指向Person元類
    • 元類的isa指針 指向 根元類(如Person元類isa指向NSObject元類
      • 根元類isa指針 指向自身(是個圓圈)
    • 元類的繼承關係向上傳遞(如Teacher元類 繼承自 Person元類
      • 根元類 繼承自 根類
      • 根類 繼承自 nil

4. 補充

4.1 isa的做用的證實2

Q:若是不用ISA_MASK,那麼如何證實isa關聯了對象和類的做用呢?

A:具體思路是,shiftclsx86_64架構下長度是44位,存儲在isa的 [3, 46]位上,因此能夠經過將isa的 [0, 2]位、[47, 63]位清零,一樣能獲得shiftcls的值,進而肯定類。

如圖所示,通過對isa的一番運算,成功獲得與Person類相同的地址。

4.2 NSProxyisa指向

Q:若是Person類繼承的是NSProxy,相關isa指向是怎樣的呢?

A:跟NSObject同樣,二者都是根類

相關文章
相關標籤/搜索