iOS原理探索03--isa與類的關聯原理

本文的主要介紹如何理解isa與類的關系,在介紹之前我們首先要知道OC對象的本質是什么?這里我們先插曲一個Clang編譯器。

一、下面是Clang介紹

Clang是?個C語?、C++、Objective-C語?的輕量級編譯器。源代碼發布于BSD協議下。
Clang將?持其普通lambda表達式返回類型的簡化處理以及更好的處理constexpr關鍵字
Clang是?個由apple主導編寫,基于LLVM的C/C++/Objective-C編譯器
2013年4?,Clang已經全??持C++11標準,并開始實現C++1y特性(也就是C++14,這是C++的下?個?更新版本)。Clang將?持其普通lambda表達式返回類型的簡化處理以及更好的處理constexpr關鍵字
Clang是?個C++編寫、基于LLVM、發布于LLVM BSD許可證下的C/C++/Objective-C/Objective-C++編譯器。它與GNU C語?規范?乎完全兼容(當然,也有部分不兼容的內容,包括編譯命令選項也會有點差異),并在此基礎上增加了額外的語法特性,?如C函數重載(通過__attribute__((overloadable))來修飾函數),其?標(之?)就是超越GCC。

  • 如何將OC文件編譯為C++文件呢?
    clang -rewrite-objc main.m -o main.cpp?標?件編譯成c++?件
  • UIKit報錯問題處理
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /
Applications/Xcode.app/Contents/Developer/Platforms/
iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m 

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將下面這段代碼編譯為cpp文件,在main中自定義一個類LGPerson,有一個屬性name
@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;
}

編譯后的文件

#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

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_99_79b11sqx7xd67mxf4rx5g0tr0000gn_T_main_949408_mi_0);
    }
    return 0;
}

  • 從編譯的main.cpp這段代碼,我們可以看出,源代碼的LGPerson編譯后是個結構體struct
    • LGPerson_IMPL中的第一個屬性 其實就是 isa,是繼承自NSObject,屬于偽繼承, 偽繼承的方式 是直接將 LGPerson作為結構體的第一個屬性,意味著LGPerson擁有 NSObject中的所有成員變量
    • LGPerson中的第一個屬性 NSObject_IVARS等效于NSObject中的isa

結論:對象的本質是一個結構體。

三、類的源碼分析

@interface NSObject <NSObject> {

    Class isa  OBJC_ISA_AVAILABILITY;
}

我們跟進源碼,發現NSObject的定義,我們發現了本文要討論的isa,由代碼可以看出isa是class類型,那么這是為什么呢?為什么isa不是其他的類型呢?
緊接著我們通過源碼調試,找到了ISA()方法,這是isa的get方法

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
}

結論:從下面代碼(Class)isa.bits(Class)(isa.bits & ISA_MASK)我們可以看出,這兩步操作都是對ISA進行了Class的強制轉換,所以isa是class類型

四、 objc_setProperty()方法解析

我們從什么是對象這一段的源碼編譯的cpp文件代碼可以看出,在添加屬性name之后,通過Clang編譯后多了兩個方法,代碼如下:

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

這里我們可以得知通過設置屬性,系統自己會添加屬性的set和get方法,并且是通過objc_setProperty方法來設置的,那么我們就來追蹤一下它的源碼,

//objc_setProperty的接口實現
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}


//reallySetProperty()接口實現
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

通過reallySetProperty()接口實現我們發現屬性的設置,只是newValue = objc_retain(newValue);新值的retain,和objc_release(oldValue);舊值的release

五、cls 的關聯原理

探索一下通過initInstanceIsa是如何將cls與isa關聯的,在此之前,需要先了解什么是聯合體????

聯合體與結構體知識小拓展

  • 結構體(struct)中所有變量是共存
    • 優點是有容乃?,全?;
    • 缺點是struct內存空間的分配是粗放的不管?不?,全分配
  • 聯合體(union)中是各變量是互斥
    • 缺點就是不夠包容
    • 優點是內存使?更為精細靈活,也節省了內存空間
  • 首先我們來看一下isa_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
};

我們可以看出isa_t是通過union聯合體來構造的,從isa_t的定義中可以看出:

  • 提供了兩個成員,cls 和 bits,由聯合體的定義所知,這兩個成員是互斥的,也就意味著,當初始化isa指針時,有兩種初始化方式

    • 通過cls初始化,bits無默認值

    • 通過bits初始化,cls有默認值

除此之外它還提供了一個結構體定義的位域,用于存儲類信息及其他信息,結構體的成員ISA_BITFIELD,這是一個宏定義,有兩個版本 __arm64__(對應ios 移動端)__x86_64__(對應macOS),以下是它們的一些宏定義,具體的宏定義代碼如下:

# 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)

# 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)

# else
#   error unknown architecture for packed isa
# endif

// SUPPORT_PACKED_ISA
#endif

先簡單介紹一下每個宏的意義:

nonpointer:表示是否對 isa 指針開啟指針優化0純isa指針1:不?是類對象地址,isa 中包含了類信息、對象的引?計數等。

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

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

shiftcls存儲類指針的值。開啟指針優化的情況下,在arm64 架構中有 33 位?來存儲類指針

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

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

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

has_sidetable_rc:當對象引?技術?于 10時,則需要借?該變量存儲進位。

extra_rc當表示該對象的引?計數值,實際上是引?計數值減 1,例如,如果對象的引?計數為 10,那么extra_rc為 9。如果引?計數?于 10,則需要使?到下?的 has_sidetable_rc

源碼追蹤

通過alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone方法路徑,查找到initInstanceIsa,并進入其原理實現。

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指針
        isa = isa_t((uintptr_t)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;
    }
}

六、isa與類的關聯

isa的關聯,主要是通過initInstanceIsa方法中的calloc指針和當前類的cls相關聯,用isa指針中的shiftcls位域來存儲的相關信息,我們可以有以下幾種驗證方式:

【方式一】通過initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3;驗證
我們通過initInstanceIsa->initIsa->newisa.shiftcls -> (uintptr_t)cls >> 3,打印查看isa指針以及查看newisa.shiftcls存儲信息來驗證是否shiftcls位域中存儲著isa指針

打印(uintptr_t)cls >> 3和newisa.shiftcls
通過控制臺的輸出結果可以看出步驟一和步驟二的信息是一致的,這就驗證我們想法。

  • 我們再來比較一下bits賦值前后的結果,bits的位域中有兩處變化cls由默認值,變成了LGPerson,將isacls完美關聯shiftcls0變成了536871965

    圖片來自Style_月月簡書----isa成員值變化過程

  • 下面我們在提出疑問為什么在shiftcls賦值時需要類型強轉

因為內存的存儲不能存儲字符串機器碼只能識別0 、1這兩種數字,所以需要將其轉換uintptr_t數據類型,這樣shiftcls中存儲的類信息才能被機器碼理解, 其中uintptr_tlong

  • 為什么需要右移3位

主要是由于shiftcls的信息處于isa指針地址中間部分前面還有3個位域,為了不影響前面的3個位域的數據,需要右移將其抹零

【方式二】通過isa指針地址與ISA_MSAK 的值 & 來驗證
cla與isa關聯后,我們通過lldb,x/4gx 在控制臺輸出obj的存儲信息,在通過obj的isa指針&MASK,結果一樣的是LGPerson


isa指針&MASK結果
  • 注意
    • arm64中,ISA_MASK 宏定義的值為0x0000000ffffffff8ULL

    • x86_64中,ISA_MASK 宏定義的值為0x00007ffffffffff8ULL

【方式三】通過runtime的方法object_getClass驗證
通過查看object_getClass的源碼實現,同樣可以驗證isa與類關聯的原理,通過runtimeapi,即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 (fastpath(!isTaggedPointer())) 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);
#endif
}

我們發現源碼一樣的是使用的isa.bits & ISA_MASK,在用Class類型做了強制轉換的。

【方式四】通過位運算驗證
回到_class_createInstanceFromZone方法。通過x/4gx obj 得到obj的存儲信息,當前的信息存儲在isa指針中,且isa中的shiftcls此時占44位(因為處于macOS環境

  • 右邊3位,和左邊除去44位以外的部分都抹零,其相對位置是不變的;
    • isa地址右移3位
    • 在將得到的結果左移20位
    • 右移17位
      操作步驟以及結果輸出
      我們可以看出同樣的可以的此結論:isa的關聯,主要是通過initInstanceIsa方法中的calloc指針和當前類的cls相關聯,用isa指針中的shiftcls位域存儲類的相關信息!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372