OC底層原理 06: isa結構分析

主動已經是我對熱愛東西表達的極限了

對象的本質?
聯合體位域的簡析?
isa的結構信息?
isa如何關聯類?
通過位運算驗證關聯類
總結。

  • 什么是對象?
    對象在底層變成了什么呢?

  • 什么是Clang?

Clang是一個由Apple主導編寫,基于LLVM的C/C++/Objective-C編譯器

  • 用Clang做些什么?

Clang 通過底層編譯,將一些m文件編譯為cpp。 因為OC為C++或者C的超集,通過Clang底層編譯,可以更多的看到底層的實現原理與邏輯和底層的架構

  • 如何使用Clang?

Clang終端操作四種命令如下:

clang -rewrite-objc main.m -o main.cpp 把目標文件編譯成c++文件

`xcode`安裝的時候順帶安裝了`xcrun`命令,`xcrun`命令在`clang`的基礎上進行了 一些封裝,要更好用一些

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (模擬器)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp (手機)

Clang 具體操作請查看:后續

通過Clang編譯查看的底層是如何實現的,首先定義如下:

  @interface LGPerson : NSObject
  @property (nonatomic, copy) NSString *name;
  @end

  @implementation LGPerson
  @end

  int main(int argc, const char * argv[]) {
      @autoreleasepool {
          // insert code here...
          NSLog(@"Hello, World!");    }
      return 0;
  }

找到已經編譯好的main.cpp文件,搜索LGPerson,得到我想要的信息如下:

  #ifndef _REWRITER_typedef_LGPerson
  #define _REWRITER_typedef_LGPerson
  typedef struct objc_object LGPerson;
  typedef struct {} _objc_exc_LGPerson;
  #endif
  
  extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
  struct LGPerson_IMPL {
          struct NSObject_IMPL NSObject_IVARS;
      NSString *_name;
  };

  // @property (nonatomic, copy) NSString *name;
  /* @end */
   // @implementation LGPerson
  static NSString * _I_LGPerson_name(LGPerson * self, SEL _cmd) { 
        return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)); 
  }

  extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

  static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) {
      objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _name), (id)name, 0, 1);
  }
  // @end
  • 對象在底層會被編譯為struct

對象是如何被編譯為結構體的呢?

我們知道結構體在C++可以繼承,C可以偽繼承

偽繼承方式:
直接將NSObject結構體定義為LGPerson中的第一個屬性,意味著LGPerson 擁有 NSObject中的所有成員變量。
LGPerson中的第一個屬性 NSObject_IVARS 等效于 NSObject中isa

這里將LGPerson_IMPL結構體中的第一屬性命名為當前結構體,即LGPerson_IMPL擁有 NSObject中的所有成員變量。而NSObject_IVARSisa

LGPerson類中的nameget, set方法也是一一對應的:

main.cpp中類對應的get,set方法截圖

set方法調用了objc_setProperty,通過分析得出objc_setProperty采用工廠模式:return newValue, remove oldValue ;意味著任何類中的set方法都會執行了如下操作:

objc4_781源碼中找到objc_setProperty查看如何執行操作get , set

繼續分析isa結構,OC底層原理 結構體&聯合體

  • isa結構分析:

在之前探索 OC底層原理 alloc & init & new 篇 時提到過initInstanceIsa方法,通過initInstanceIsa為切入點來分析isa是如何初始化的,可以看到isa指針的類型isa_t定義是通過聯合體(union)來定義,聯合體不清楚的小伙伴請參考:OC底層原理 結構體&聯合體

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

    Class cls; //這里返回的cls 為什么會是一個class類型?請查看后續
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

這里的isa_t為什么要定義成聯合體呢?是為了優化內存,而isa指針占用的內存大小為8字節,即64位,已經能夠存儲大量信息了,這樣可以節省內存空間,以提高性能的目的

isa_t的定義中可以看出,
通過cls初始化,bits無默認值
通過bits初始化,cls有默認值
體現了聯合體互斥性

通過宏ISA_BITFIELD可以看出isaiOS( __arm64__)macOS(__x86_64__)的計算是存在差異

isa不同環境的差異化截圖
  • isa的結構信息對應如下:
    • nonpointer:表示是否對 isa 指針開啟指針優化
      0:純isa指針;
      1:不止是類對象地址,isa 中包含了類信息、對象的引用計數等

    • has_assoc:關聯對象標志位,
      0沒有關聯對象
      1存在關聯對象

    • has_cxx_dtor:該對象是否有 C++ 或者 Objc 的析構器(類似于dealloc
      如果析構函數,則需要做析構邏輯
      如果沒有,則可以更快的釋放對象

    • shiftcls:存儲類指針的值(類的地址), 即類信息開啟指針優化的情況下,
      __arm64__ 架構中有 33 位用來存儲類指針。
      __x86_64__ 架構中有 44 位用來存儲類指針。

    • magic:用于調試器判斷當前對象是真的對象還是沒有初始化的空間

    • weakly_referenced:志對象是否被指向或者曾經指向一個 ARC 的弱變量
      沒有弱引用的對象可以更快釋放。

    • deallocating:標志對象是否正在釋放內存

    • has_sidetable_rc:當對象引用計數大于 10時,則需要借用該變量存儲進位

    • extra_rc:當表示該對象的引用計數值,實際上是引用計數值減 1, 例如,如果對象的引用計數為 10,那么 extra_rc9。如果引用計數大于 10, 則需要使用 has_sidetable_rc

更直觀的isa的結構信息圖如下:

isa結構對照表

通過 initIsa方法,查看 isa指針是如何被初始化

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)cls);//isa初始化
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

        isa_t newisa(0);  //isa初始化
#if SUPPORT_INDEXED_ISA  // !nonpinter 執行,即isa通過cls定義
        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 // bits 時執行的流程
        newisa.bits = ISA_MAGIC_VALUE; // bits進行賦值
        // 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; //isa與類關聯
#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;
    }
}
  • initIsa方法主要使用:cls初始化isa,和bits初始化isa

執行代碼,開始驗證,以LGPerson為例:

對賦值newisa.bits賦值之后打印出newisa查看LLDB結果

p newisa 打印結果

  • 對賦值newisa.bits賦值會對cls進行追加;對cls賦值不會同時對bits進行賦值(聯合體互斥性

通過LLDB打印結果,在進行賦值的時候,bits中的magic會存在一個59呢?
使用計算器查看宏ISA_MAGIC_VALUE592進制

為什么magic為59

magic的占位為6,從47~52表示magic的占位,所以magic的默認值為59,二進制表示;也可以理解為isa指針將magic的占位為6,轉換為2進制存儲

  • isa與類關聯

當執行完如下代碼后,LLDB打印出當前newisa,
clsisa 關聯原理就是isa指針中的shiftcls位域中存儲了類信息,其中initInstanceIsa的過程是將 calloc 指針 和當前的 類cls 關聯起來。而newisa.shiftcls = (uintptr_t)cls >> 3 是將當前cls 強轉為系統能夠編譯的01識別碼。然后右移3位。之所以要移動3位,要知道shiftcls才是我們需要存儲的類信息,從上面isa對照表中可以看出,前面三位并不是我們需要的類內容,需要右移3位進行抹0操作。

newisa.shiftcls = (uintptr_t)cls >> 3; //isa與類關聯
isa與類關聯結果圖

這個時候我們已經知道isa 進行了關聯。

  • 開始驗證isa中的shiftcls是否真正的存儲了當前類信息

initInstanceIsa方法返回出去,然后開始驗證:

  • 方式1. 通過isa指針地址與宏ISA_MSAK 的值 通過位運算& 來驗證
      #   define ISA_MASK        0x00007ffffffffff8ULL    //__x86_64__ 
      #   define ISA_MASK        0x0000000ffffffff8ULL    //__arm64__
    
    LLDB 操作:po obj ,然后x/4gx LGPerson的地址 或者直接x/4gx,然后取LGPersonisa地址指針 &ISA_MASK,結果如下:
    位運算驗證
  • 方式2. 使用位運算驗證
    shiftcls存儲在當前3~46位,
    向右移動3位,抹除0~2號位數據,
    向左移動20位,移除47~64號位的數據,
    向右移動17位,回到原來的shiftcls所在的存儲位,直接取出,即LGPerson 類信息
    位運算操作截圖
  • 方式3. 通過runtimeobject_getClass來驗證
    main.m導入頭文件#import <objc/runtime.h>,然后直接打印出當前的類也能夠驗證,這里就不打印了,直接去查看object_getClass的實現,
    object_getClass方法如下:
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

我們進入getIsa方法繼續查看,

inline Class 
objc_object::getIsa() 
{
    if (fastpath(!isTaggedPointer())) return ISA(); //直接return了當前isa

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}

繼續查看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); //這里與我們方式1做的操作是一樣的
#endif
}

我們可以看出object_getClass的內部實現,也是采用了位運算&的方式對當前類進行處理。

驗證總結:驗證isa與類是否關聯還有其他方式,這里不一一闡述,有興趣的同學可以自己探索。

  • isa結構分析總結:
    1.isa通過isa_t方法進行初始化,其中使用到了聯合體位域;isa關聯的同時,做了強轉的操作和位運算計算存儲,其中newisa.shiftcls = (uintptr_t)cls >> 3;uintptr_t表示long類型。
    2.x86_64arm64環境不同,存在部分計算差異
    3.掌握如何對isa的關聯驗證
    4.如何使用Clang查看源碼。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。