《跟我學(xué)》之OC對(duì)象isa結(jié)構(gòu)分析

1. 前言

上一篇我們了解到了一個(gè)對(duì)象的屬性?xún)?nèi)存分配和占用情況。并且額外引入了兩個(gè)結(jié)構(gòu)體做了對(duì)比。我們發(fā)現(xiàn)類(lèi)結(jié)構(gòu)體好像有什么相似的地方。那到底有什么相似的呢。話(huà)不多說(shuō),肝著。

1.1Clang

首先clang是一個(gè)由Apple主導(dǎo)編寫(xiě),基于LLVMC/C++/OC的編譯器,這貨干啥的呢?
主要用途可以將你編寫(xiě)的類(lèi)輸出成較為低一級(jí)別的代碼,第一天玩人(Person)。第二天玩狗(Dog),今天我們來(lái)當(dāng)許仙。一起來(lái)玩蛇(Snake)??,例如將你Snake.m 輸出為Snake.cpp,這樣一來(lái)就可以更直觀的觀察到代碼還做了哪些你不知道的事情。直接上碼

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

@implementation Snake
@end

通過(guò)終端,利用 clangSnake.m 編譯成 Snake.cpp,有以下幾種編譯命令,這里使用的是第一種

//1、將 Snake.m 編譯成 Snake.cpp
clang -rewrite-objc Snake.m -o Snake.cpp

//2、將 ViewController.m 編譯成  ViewController.cpp
**這里要注意`iPhoneSimulator13.7`這個(gè)目錄一定要跟你本地的目錄對(duì)應(yīng)上**
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

//以下兩種方式是通過(guò)指定架構(gòu)模式的命令行,使用 `xcode` 工具 `xcrun`
//3、模擬器文件編譯
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc Snake.m -o Snake-arm64.cpp 

//4、真機(jī)文件編譯
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Snake.m -o Snake- arm64.cpp 

之后我們會(huì)在同級(jí)文件看到Snake.cpp文件。打開(kāi)之后是不是很驚喜有上萬(wàn)行代碼。驚不驚喜,意不意外。
我們?nèi)炙阉髦豢次覀冴P(guān)心部分。

extern "C" unsigned long OBJC_IVAR_$_Snake$_name;
struct Snake_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
};
/* @end */
// @interface Snake ()
// @property (nonatomic, copy) NSString *name;
/* @end */
// @implementation Snake
//手動(dòng)添加的注釋?zhuān)瑢?duì)應(yīng)name的geet方法
static NSString * _I_Snake_name(Snake * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Snake$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
//手動(dòng)添加的注釋?zhuān)瑢?duì)應(yīng)name的set方法
static void _I_Snake_setName_(Snake * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Snake, _name), (id)name, 0, 1); }
// @end

1.2分析

我們剛才OC代碼定義的Snake類(lèi)以及屬性居然被注釋了,等價(jià)的被替換成了C++代碼。并且我們的類(lèi)變成了結(jié)構(gòu)體,我們都知道萬(wàn)物皆NSObject,我們的這個(gè)Snake類(lèi)也是繼承NSObject,但是定義的 Snake 類(lèi)只有一個(gè)name屬性,為什么結(jié)構(gòu)體里還有 NSObject_IMPL的結(jié)構(gòu)體呢?

其實(shí)這樣的定義同OC,也是繼承自 NSObject的意思 ,屬于偽繼承,偽繼承的方式是直接將 NSObject 結(jié)構(gòu)體定義為 Snake 中的第一個(gè)屬性,意味著 Snake 擁有 NSObject 中的所有成員變量
Snake 中的第一個(gè)屬性 NSObject_IVARS 等效于 NSObject 中的 isa

我們多次聽(tīng)到了這個(gè) isa。這個(gè) isa 到底是做啥的,平時(shí)開(kāi)發(fā)好像也沒(méi)怎么用到它,為什么會(huì)被多次提及,引用大佬的一句話(huà)簡(jiǎn)單來(lái)說(shuō)就是很重要,裝逼的來(lái)說(shuō)不要試圖去理解它。試著去感受它。

還記得我們提及過(guò) alloc 三大核心方法的核心之一的 initInstanceIsa 方法嗎?忘記了沒(méi)關(guān)系,上祖?zhèn)鞔a

obj->initInstanceIsa(cls, hasCxxDtor);
-------------------------------------------------
inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        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;
    }
}

我們看到這個(gè)方法有點(diǎn)懵逼。那我們一層脫下它的衣服??纯此锩娲┝松?br> 1、 通過(guò)cls初始化isa
2、如果是非 nonpointer,代表普通的指針,存儲(chǔ)著 Class、Meta-Class 對(duì)象的內(nèi)存地址信息。
3、然后就發(fā)現(xiàn) 定義了一個(gè)newisa,然后對(duì)它瘋狂賦值。足已證明它多重要了。我們看看里面是什么

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.3 結(jié)構(gòu)體(struct)&&聯(lián)合體(union)

構(gòu)造數(shù)據(jù)類(lèi)型的方式有以下兩種:

  • 結(jié)構(gòu)體(struct
  • 聯(lián)合體(union,也稱(chēng)為共用體)
    之前我們已經(jīng)講過(guò) struct,現(xiàn)在又出現(xiàn)一種union我們來(lái)好好科普一下這兩個(gè)東西

結(jié)構(gòu)體

結(jié)構(gòu)體是指把不同的數(shù)據(jù)組合成一個(gè)整體,其變量共存的,變量不管是否使用,都會(huì)分配內(nèi)存。

缺點(diǎn):所有屬性都分配內(nèi)存,比較浪費(fèi)內(nèi)存,假設(shè)有 `4` 個(gè) `int` 成員,一共分配了 `16` 字節(jié)的內(nèi)存,但是在使用時(shí),你只使用了 `4` 字節(jié),剩余的 `12` 字節(jié)就是屬于內(nèi)存的浪費(fèi)
優(yōu)點(diǎn):存儲(chǔ)容量較大,包容性強(qiáng),且成員之間不會(huì)相互影響

聯(lián)合體

聯(lián)合體 也是由不同的數(shù)據(jù)類(lèi)型組成,但其變量是互斥的,所有的成員共占一段內(nèi)存。而且共用體采用了內(nèi)存覆蓋技術(shù),同一時(shí)刻只能保存一個(gè)成員的值,如果對(duì)新的成員賦值,就會(huì)將原來(lái)成員的值覆蓋掉

缺點(diǎn):包容性弱
優(yōu)點(diǎn):所有成員共用一段內(nèi)存,使內(nèi)存的使用更為精細(xì)靈活,同時(shí)也節(jié)省了內(nèi)存空間

兩者的區(qū)別

1、內(nèi)存占用情況

結(jié)構(gòu)體的各個(gè)成員會(huì)占用不同的內(nèi)存,互相之間沒(méi)有影響
共用體的所有成員占用同一段內(nèi)存,修改一個(gè)成員會(huì)影響其余所有成員

2、內(nèi)存分配大小

結(jié)構(gòu)體內(nèi)存 >= 所有成員占用的內(nèi)存總和(成員之間可能會(huì)有縫隙)
共用體占用的內(nèi)存等于最大的成員占用的內(nèi)存

我們剛才的那個(gè)isa_t就是一個(gè)union,為什么使用它來(lái)定義。通過(guò)剛才優(yōu)缺點(diǎn)也自然不言而喻了。我們來(lái)分析一下isa_t這個(gè)里面定義了什么?

  • cls:是Class類(lèi)型的指針變量,指向的是對(duì)象的類(lèi)。
  • bits:是結(jié)構(gòu)體位域指針。
  • ISA_BITFIELD:宏 ISA_BITFIELD,用來(lái)定義位域,用于存儲(chǔ)類(lèi)信息及其他信息。

ISA_BITFIELD

ISA_BITFIELD 宏在內(nèi)部分別定義了arm64位架構(gòu)(iOS)和x86_64架構(gòu)(macOS)的掩碼和位域.。

圖1

isa的存儲(chǔ)情況如圖所示

圖2

現(xiàn)在也就理解剛才代碼中newisa賦值都是干啥的了吧。
1、clsisa關(guān)聯(lián)原理就是isa指針中的shiftcls位域中存儲(chǔ)了類(lèi)信息,
2、initInstanceIsa的過(guò)程是將創(chuàng)建對(duì)象的指針和當(dāng)前的 類(lèi)cls 關(guān)聯(lián)起來(lái)

最后

說(shuō)了這么多。我們是否能裝逼反響驗(yàn)證一波上面所說(shuō)的呢?
1、【方式一】通過(guò)initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3來(lái)驗(yàn)證
2、【方式二】通過(guò)isa指針地址與ISA_MSAK 的值 & 來(lái)驗(yàn)證
3、【方式三】通過(guò)runtime的方法object_getClass驗(yàn)證
4、【方式四】通過(guò)位運(yùn)算驗(yàn)證

方式一:通過(guò) initIsa 方法

newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;

我們用源代碼在這兩行代碼加入斷點(diǎn)。確保調(diào)用傳遞進(jìn)來(lái)的cls是我們要研究的Snake類(lèi)
運(yùn)行至此時(shí)。在lldb做以下操作

圖3

聰明的你是不是已經(jīng)發(fā)現(xiàn),我們p (uintptr_t)cls,結(jié)果為(uintptr_t) $5 = 4294976016,再右移三位,p (uintptr_t)cls >> 3得到(uintptr_t) $6 = 536872002,我們?cè)僭噷?5的值右移3位p 4294976016 >> 3,得到也是536872002,最后從左邊變量看shiftcls還是我們來(lái)直接暴力的看一下p newisa.shiftcls得到也是536872002
cls也變成了我們的Snake

方式二:通過(guò) isa & ISA_MSAK

if (!zone && fast) {
    obj->initInstanceIsa(cls, hasCxxDtor);
} else {
    // Use raw pointer isa on the assumption that they might be
    // doing something weird with the zone or RR.
    obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
  return obj;
}

我們走完剛才的方法返回到這里時(shí)在要return obj的之前的地方打個(gè)斷點(diǎn)。執(zhí)行x/4gx obj,得到isa指針的地址0x001d800100002215,再將isa指針地址 & ISA_MASK (處于macOS,使用x86_64中的宏定義),即 po 0x001d800100002215 & 0x00007ffffffffff8 ,得出Snake

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

方式三:通過(guò) object_getClass

通過(guò)查看·object_getClass·的源碼實(shí)現(xiàn),最終發(fā)現(xiàn)核心處理與我們的方法二一樣。這里就不過(guò)多復(fù)述

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
}

方式四:通過(guò)位運(yùn)算

我們用方法二在返回obj之前斷點(diǎn)執(zhí)行如下操作

1、將isa地址右移3位:p/x 0x001d800100002215 >> 3 ,得到0x0003b00020000442
2、再將得到的0x0003b00020000442左移20位:p/x 0x0003b00020000442 << 20 ,得到0x0002000044200000
3、將得到的0x0002000044200000 再右移17位:p/x 0x0002000041d00000 >> 17 得到新的0x0000000100002210

我們之所以左移右移,是因?yàn)橹?code>shiftcls所在位于的位置。所有的操作都是為了精準(zhǔn)讀取到shiftcls
那為什么是左移20位?因?yàn)橄扔乙屏?code>3位,相當(dāng)于向右偏移了3位,而左邊需要抹零的位數(shù)有17位,所以一共需要移動(dòng)20

獲取cls的地址,或者直接po 與上面的進(jìn)行驗(yàn)證 得到

p/x cls
0x0000000100002210 `Snake`
po 0x0000000100002210 `Snake`

(注:部分圖片來(lái)自“style_月月”的博客) 傳送門(mén)->Style_月月

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容