深入理解Objective-C-Runtime-isa

RiverLi發表於2019-02-23

本文首發自RiverLi的公眾號

Objective-C的動態性

我們都知道OC是一門動態性的語言,那麼怎麼理解動態性呢?動態性是指能將操作推遲到執行時再執行,所謂的執行時是指程式碼經過編譯、連結之後,執行的狀態。OC的動態性是由Runtime實現的。Runtime是一套C語言API,封裝著很多動態性相關的函式。動態性有很多實踐應用,比如:

  • 利用關聯物件(AssociatedObject)給分類新增屬性
  • 遍歷類的所有成員變數。
  • 交換方法實現。
  • 利用訊息轉發機制解決方法找不到的異常問題
  • 等等

isa詳解

我們知道OC中有類與元類之說,如下圖是經典的物件關係圖,有圖可知:

  1. 對於物件來說isa指向其類。
  2. 對於類物件來說isa指向其元類。
    OC類結構圖

下面我們通過閱讀原始碼看物件的內部資訊。

  1. 首先,在NSObject.h檔案中我們可以看到如下資訊
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
複製程式碼
  1. 我們將 main.m通過clang工具可以將OC原始碼編譯為CPP程式碼
//編譯命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

//main.m
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
}
複製程式碼

通過觀察 main-arm64.cpp 檔案我們可以發現如下資訊:

//1.
typedef struct objc_object NSObject;

//2.
struct NSObject_IMPL {
	Class isa;
};
複製程式碼
  1. 通過閱讀runtime原始碼,我們可以獲得如下資訊
//objc-runtime-new.h
struct objc_class : objc_object {
    Class superclass;  
    ...
}

// objc-private.h
typedef struct objc_class *Class;
typedef struct objc_object *id;

struct objc_object {
private:
    isa_t isa;
    ...
}
複製程式碼

我們可以得到如下結論:

  • NSObject其實是struct objc_object型別。
  • objc_class繼承自objc_object型別。
  • Class其實是struct objc_class型別。
  • objc_object內部含有一個isa變數,其型別是isa_t。

接下來我們繼續看isa的實現,如下:

// objc-private.h
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
};

// isa.h
# 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
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

//objc-class.mm
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

//objc-object.h
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];
    }
}

//objc-object.h
#if SUPPORT_NONPOINTER_ISA
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
}
// SUPPORT_NONPOINTER_ISA
#else
// not SUPPORT_NONPOINTER_ISA


inline Class 
objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
    return isa.cls;
}
複製程式碼

由上可知isa是由union結構實現的,在獲取isa內容的時候通過SUPPORT_NONPOINTER_ISA巨集來指定不同的取法。下面看巨集的定義

#if !SUPPORT_INDEXED_ISA  &&  !SUPPORT_PACKED_ISA
#   define SUPPORT_NONPOINTER_ISA 0
#else
#   define SUPPORT_NONPOINTER_ISA 1
#endif

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

#if (!__LP64__  ||  TARGET_OS_WIN32  ||  \
     (TARGET_OS_SIMULATOR && !TARGET_OS_IOSMAC))
#   define SUPPORT_PACKED_ISA 0
#else
#   define SUPPORT_PACKED_ISA 1
#endif
複製程式碼

通過參考1、參考2,我們可以等到在針對arm64的iphone裝置上,SUPPORT_INDEXED_ISA=0,SUPPORT_PACKED_ISA=0,SUPPORT_NONPOINTER_ISA=0, SUPPORT_INDEXED_ISA=0。 從而object_getClass的返回值是 a.bits & ISA_MASK`

isa其他欄位意義

  • nonpointer:0,代表普通的指標,儲存著Class、Meta-Class物件的記憶體地址。 1,代表優化過,使用位域儲存更多的資訊
  • has_assoc:是否有設定過關聯物件,如果沒有,釋放時會更快
  • has_cxx_dtor:是否有C++的解構函式(.cxx_destruct),如果沒有,釋放時會更快
  • shiftcls:儲存著Class、Meta-Class物件的記憶體地址資訊
  • magic: 用於在除錯時分辨物件是否未完成初始化
  • weakly_referenced:是否有被弱引用指向過,如果沒有,釋放時會更快
  • deallocating:物件是否正在釋放
  • extra_rc:裡面儲存的值是引用計數器減1
  • has_sidetable_rc:引用計數器是否過大無法儲存在isa中,如果為1,那麼引用計數會儲存在一個叫SideTable的類的屬性中。

結論

  1. 物件的isa指向類,類的isa指標指向元類,元類的isa指標指向基類。
  2. isa是用union結構表示, 根據不同的平臺isa所指向的物件的地址取法不一樣,如arm64環境下,真實地址是:isa.bits & ISA_MASK

參考

參考1
參考2
參考3:共用體