iOS的OC的isa的底層原理

前言

一步一個腳印地探索iOS的OC底層原理,經過前面的文章能夠大概瞭解了OC對象建立的alloc原理OC對象的內存字節對齊,可是這也只是知道了對象建立的底層過程和開闢內存空間的,這篇文章將介紹對象的本質和對象與類的關聯---isabash

1.isa的初始化

isa指針:在Objective-C中,任何類的定義都是對象。類和類的實例(對象)沒有任何本質上的區別。任何對象都有isa指針。也就是說在對象建立的時候就會有isa指針初始化了。爲了搞清楚仍是須要用到OC對象建立的alloc原理裏面源碼的_class_createInstanceFromZone的方法的部分源碼,而後跟着流程進去獲得以下的部分源碼數據結構

//_class_createInstanceFromZone的部分代碼
//初始化實例的isa指針
 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);

#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; } } 複製代碼

其中的nonpointer字面的意思是沒有指針的,通常狀況下nonpointer是爲true的,只有在例如實現了allocwithzone方法,retain,release等的時候會是false。若是爲false是直接將傳進來的cls爲isa的關聯的cls賦值。架構

其餘的剩下的部分就是對isa的初始化賦值了。可是具體的isa內部是怎樣的仍是不知道的,從源碼中isa_t點擊進去能夠查看。ide

2.isa的內部結構

經過源碼能夠知道isaisa_t類型的內部結構函數

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

#ifndef _UINTPTR_T
#define _UINTPTR_T
typedef unsigned long           uintptr_t;
#endif /* _UINTPTR_T */

複製代碼

從中能夠知道,isa是一個聯合體(union),裏面有關聯的類cls和long型的bits。源碼分析

什麼是聯合體(union)呢?聯合體是一種特殊的類,也是一種構造類型的數據結構。徹底就是共用一個內存首地址,而且各類變量名均可以同時使用,操做也是共同生效。因此也叫共用體。而且聯合體(union)中是各變量是「互斥」的,可是內存使用更爲精細靈活,也節省了內存空間。post

由上面的概念能夠知道,clsbits之間是互斥的,即有cls就沒有bits,有bits就沒有cls。這就很好地解釋了爲何上面的源碼在初始化isa的時候會用nonpointer來區分開。因此isa的大小佔8個字節,64位。其中這64位中分別存儲了什麼呢?經過ISA_BITFIELD位域源碼:優化

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
      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

# 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
複製代碼

這兩種是分別在arm64x86系統架構下的,可是都是64位的,本文的說明是在x86下介紹的。ui

  • nonpointer:佔1位,表示是否對isa指針開啓指針優化,0:表示純指針;1:表示不止是類對象地址,isa中包含了類信息、對象的引用計數等。
  • has_assoc:佔1位,表示關聯對象標誌位,0沒有,1存在。
  • has_cxx_dtor:佔1位,表示該對象是否有C++或者Objc的析構器,若是有析構函數,則須要作析構邏輯,若是沒有則能夠更快的釋放對象。
  • shiftcls:佔44位,存儲類指針的值。開啓指針優化的狀況下,在arm64架構中有33位用來存儲類指針。(由上面的初始化源碼也能夠很好的說明,關聯的類指針向右移3位)
newisa.shiftcls = (uintptr_t)cls >> 3;
複製代碼
  • magic:佔6位,用於調試器判斷當前對象是真的對象仍是沒有初始化的空間。
  • weakly_referenced:佔1位,表示對象是否被指向或者曾經指向一個ARC的弱變量,沒有弱引用的對象能夠更快釋放。
  • deallocating:佔1位,表示對象是否正在釋放內存。
  • has_sidetable_rc:佔1位,表示當對象引用技術大於10時,則須要借用該變量存儲進位。
  • extra_rc:佔8位,表示該對象的引用計數值,其實是引用計數總值減1。例如,若是對象的引用計數爲10,那麼extra_rc爲9,若是引用計數大於10,則須要使用到下面的has_sidetable_rc。

3.isa是對象與類的鏈接

爲了方便介紹下面的內容須要定義一個什麼屬性都沒有的TestJason類,而後經過objc4-756.2蘋果官方的源碼。經過object_getClass這個方法能夠獲取到類。this

TestJason *test2 = [TestJason alloc];
Class testClass = object_getClass(test2);
NSLog(@"%@",test2);
複製代碼

經過源碼找到object_getClass的方法

/***********************************************************************
* object_getClass.
* Locking: None. If you add locking, tell gdb (rdar://7516456).
**********************************************************************/
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

inline Class 
objc_object::getIsa() 
{
    if (!isTaggedPointer()) return ISA();

    uintptr_t ptr = (uintptr_t)this;
    if (isExtTaggedPointer()) {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        return objc_tag_ext_classes[slot];
    } else {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
}

inline Class 
objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

# define SUPPORT_INDEXED_ISA 0
# define ISA_MASK 0x00007ffffffffff8ULL

複製代碼

從源碼中能夠知道返回的isa最終是(Class)(isa.bits & ISA_MASK)。 其中源碼有一個判斷isTaggedPointer(),其中蘋果對於Tagged Pointer的概念引入,是爲了節省內存和提升執行效率,對於 64 位程序,相關邏輯能減小一半的內存佔用,以及 3 倍的訪問速度提高,100 倍的建立、銷燬速度提高。若是想了解這部分的內容能夠看看深刻理解 Tagged Pointer

下面就是用lldb的指令來驗證一下的。首先用x/4gx test2來打印出地址

而後用 p/x TestJason.class打印出 TestJason類的內存值

由源碼知道類Class的最終返回是 (Class)(isa.bits & ISA_MASK)的,因此將 x/4gx test2打印出來的 0x001d800100001749 & ISA_MASK的值獲得以下:

最終發現 $3$4的內存值是同樣的,因此 isa是關聯着對象與類的。由前面的文章知道因爲內存的優化對象的其餘屬性的位置實際會發生變化的,因此對象的第一個屬性就是 isa

4.isa的走位原理

經過上面的介紹,能夠知道了isa是關聯着對象與類的,而且對象的isa指向類,由於萬物皆對象,那麼類的isa指向的是誰呢?能夠經過蘋果官方的isa的走位流程圖 isa流程圖.png

其中虛線是表明 isa的走位,實線表明的是 繼承關係的走位。圖中有一個 meta class元類的概念。

什麼是元類?在OC中,對象的方法並無存儲於對象的結構體中(若是每個對象都保存了本身能執行的方法,那麼對內存的佔用有極大的影響)。 當對象的實例方法被調用時,它經過本身的isa來查找對應的類,而後在所屬類的 class_data_bits_t結構體中查找對應方法的實現。同時,每個objc_class 也有一個指向本身的父類的指針superclass用來查找繼承的方法。 而當調用 類方法 時,它的查找流程是怎樣的呢?對此OC的解決方案就是引入元類,來保證類方法也能經過相同的機制查找到。對於元類的解釋轉自OC源碼分析之isa

是否是看着這張圖的各類箭頭指向一臉懵逼呢?下面就是來對這張圖的isa的走位驗證一下。

4.1 isa的走位

仍是用TestJason這個什麼屬性都沒有的類來介紹。經過lldb的指令來驗證。由上面的圖能夠知道,對象的isa指向類,類的isa指向元類。

由圖能夠知道,先用 x/4gx test2來打印出 isa的內存值,而後用 isa的內存值& ISA_MASK獲得 $9,而後 po $9此時獲得的是類。而後再用 x/4gx來打印 $9的值,此時就是至關於打印出類的 isa的內存值了。最後能夠看到兩個 TestJason,可是這兩個的內存值是不同的,分別是類和元類。可是中能夠看到 元類裏面還有 isa值,那麼就繼續打印。

從中能夠看到,元類的 isa指向了 NSObject,可是這個 NSObject究竟是類呢?仍是元類呢?爲了搞清楚,能夠用 x/4gx NSObject.class來打印類的 isa來驗證。

從中能夠看到 $13打印的內存值是等於 $16的,那麼就能夠知道 元類isa指向的是 根元類isa。那麼 根元類isa指向誰呢?繼續打印

從中能夠看到 根元類isa指向了它的自己,這就造成了一個閉環。因此總體來講的話,就是

對象的isa-->類的isa-->元類的isa-->根元類的isa-->根元類的isa(根元類自己)
複製代碼

這就很好地驗證了蘋果的官網的isa走位圖。

4.2 對象,類之間的繼承的走位

爲了介紹方便,添加多一個TestSuperJason的類,而且讓TestJason是繼承TestSuperJason的。仍是經過lldb的指令來的打印class_getSuperclass方法。

這是類的繼承關係的打印

這是元類的繼承關係打印

因此它們的繼承關係是

類:
TestJason-->TestSuperJason-->NSObject-->nil
元類:
TestJason元類-->TestSuperJason元類-->NSObject元類-->NSObject類-->nil
複製代碼

5.對象的本質

經過上面的知識能夠大概瞭解了isa的原理,可是對象的本質是什麼還不是很瞭解的,能夠經過clang編譯成cpp文件來查看。實現的代碼以下:

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

@implementation Jason

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"123");
    }
    return 0;
}
複製代碼

在文件的路徑下,能夠終端輸入命令,就能夠查看main.cpp文件

clang -rewrite-objc main.m -o main.cpp
複製代碼

從中能夠看到

#ifndef _REWRITER_typedef_Jason
#define _REWRITER_typedef_Jason
typedef struct objc_object Jason;
typedef struct {} _objc_exc_Jason;
#endif

extern "C" unsigned long OBJC_IVAR_$_Jason$_name;
struct Jason_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *nickName;
	NSString *_name;
};

// @property (nonatomic, copy) NSString *name;
/* @end */


// @implementation Jason


static NSString * _I_Jason_name(Jason * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Jason$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_Jason_setName_(Jason * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Jason, _name), (id)name, 0, 1); }
// @end


struct NSObject_IMPL {
	Class isa;
};

複製代碼

從中能夠看到,對象最終會被編譯成結構體struct,NSObject_IMPL裏面包含着isa,在類裏面定義的屬性name和成員變量nickName也是在Jason_IMPL結構體裏面,可是屬性變量name是有getter方法_I_Jason_namesetter方法_I_Jason_setName_的,而成員變量nickName是沒有的。而且這些方法裏面都有默認帶有兩個參數id selfSEL _cmd,這樣就很好解釋了咱們在方法中能夠直接調用self

5.最後

經過上面的內容能夠了解到isa是一個isa_t類型的聯合體(union),而且裏面的屬性是互斥的,isa的大小佔8字節。通常狀況下都是在bits下的位域來存儲內容,其中ISA_BITFIELDx86arm64架構下都是64位,可是裏面的屬性的佔位有點區別的。isa是關聯着對象與類的,而且對象的本質就是一個結構體。至此有關isa的原理介紹到此結束。

相關文章
相關標籤/搜索