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

1. 前言

上一篇我們了解到了一個對象的屬性內(nèi)存分配和占用情況彩掐。并且額外引入了兩個結(jié)構(gòu)體做了對比裁厅。我們發(fā)現(xiàn)結(jié)構(gòu)體好像有什么相似的地方。那到底有什么相似的呢哑芹。話不多說,肝著捕透。

1.1Clang

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

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

@implementation Snake
@end

通過終端盟榴,利用 clangSnake.m 編譯成 Snake.cpp,有以下幾種編譯命令嘉冒,這里使用的是第一種

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

//2咆繁、將 ViewController.m 編譯成  ViewController.cpp
**這里要注意`iPhoneSimulator13.7`這個目錄一定要跟你本地的目錄對應(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

//以下兩種方式是通過指定架構(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 

之后我們會在同級文件看到Snake.cpp文件银觅。打開之后是不是很驚喜有上萬行代碼。驚不驚喜坏为,意不意外究驴。
我們?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
//手動添加的注釋匀伏,對應(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);
//手動添加的注釋洒忧,對應(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類以及屬性居然被注釋了,等價的被替換成了C++代碼够颠。并且我們的類變成了結(jié)構(gòu)體熙侍,我們都知道萬物皆NSObject,我們的這個Snake類也是繼承NSObject履磨,但是定義的 Snake 類只有一個name屬性蛉抓,為什么結(jié)構(gòu)體里還有 NSObject_IMPL的結(jié)構(gòu)體呢?

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

我們多次聽到了這個 isa笑跛。這個 isa 到底是做啥的付魔,平時開發(fā)好像也沒怎么用到它,為什么會被多次提及飞蹂,引用大佬的一句話簡單來說就是很重要抒抬,裝逼的來說不要試圖去理解它。試著去感受它晤柄。

還記得我們提及過 alloc 三大核心方法的核心之一的 initInstanceIsa 方法嗎?忘記了沒關(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;
    }
}

我們看到這個方法有點懵逼。那我們一層脫下它的衣服芥颈』堇眨看看它里面穿了啥
1、 通過cls初始化isa
2爬坑、如果是非 nonpointer,代表普通的指針,存儲著 Class纠屋、Meta-Class 對象的內(nèi)存地址信息。
3盾计、然后就發(fā)現(xiàn) 定義了一個newisa售担,然后對它瘋狂賦值。足已證明它多重要了署辉。我們看看里面是什么

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ù)類型的方式有以下兩種:

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

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

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

缺點:所有屬性都分配內(nèi)存逝淹,比較浪費內(nèi)存,假設(shè)有 `4` 個 `int` 成員桶唐,一共分配了 `16` 字節(jié)的內(nèi)存栅葡,但是在使用時,你只使用了 `4` 字節(jié)尤泽,剩余的 `12` 字節(jié)就是屬于內(nèi)存的浪費
優(yōu)點:存儲容量較大欣簇,包容性強(qiáng),且成員之間不會相互影響

聯(lián)合體

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

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

兩者的區(qū)別

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

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

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

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

我們剛才的那個isa_t就是一個union,為什么使用它來定義。通過剛才優(yōu)缺點也自然不言而喻了如贷。我們來分析一下isa_t這個里面定義了什么陷虎?

  • cls:是Class類型的指針變量,指向的是對象的類杠袱。
  • bits:是結(jié)構(gòu)體位域指針尚猿。
  • ISA_BITFIELD:宏 ISA_BITFIELD,用來定義位域楣富,用于存儲類信息及其他信息凿掂。

ISA_BITFIELD

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

圖1

isa的存儲情況如圖所示

圖2

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

最后

說了這么多糠涛。我們是否能裝逼反響驗證一波上面所說的呢?
1耙旦、【方式一】通過initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3來驗證
2脱羡、【方式二】通過isa指針地址與ISA_MSAK 的值 & 來驗證
3、【方式三】通過runtime的方法object_getClass驗證
4免都、【方式四】通過位運算驗證

方式一:通過 initIsa 方法

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

我們用源代碼在這兩行代碼加入斷點。確保調(diào)用傳遞進(jìn)來的cls是我們要研究的Snake
運行至此時帆竹。在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险领,我們再試將$5的值右移3位p 4294976016 >> 3,得到也是536872002秒紧,最后從左邊變量看shiftcls還是我們來直接暴力的看一下p newisa.shiftcls得到也是536872002
cls也變成了我們的Snake

方式二:通過 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;
}

我們走完剛才的方法返回到這里時在要return obj的之前的地方打個斷點绢陌。執(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

方式三:通過 object_getClass

通過查看·object_getClass·的源碼實現(xiàn)秤掌,最終發(fā)現(xiàn)核心處理與我們的方法二一樣愁铺。這里就不過多復(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
}

方式四:通過位運算

我們用方法二在返回obj之前斷點執(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

我們之所以左移右移孟岛,是因為知道shiftcls所在位于的位置瓶竭。所有的操作都是為了精準(zhǔn)讀取到shiftcls
那為什么是左移20位?因為先右移了3位渠羞,相當(dāng)于向右偏移了3位在验,而左邊需要抹零的位數(shù)有17位,所以一共需要移動20

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

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

(注:部分圖片來自“style_月月”的博客) 傳送門->Style_月月

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腋舌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子渗蟹,更是在濱河造成了極大的恐慌块饺,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雌芽,死亡現(xiàn)場離奇詭異授艰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)世落,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門淮腾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人屉佳,你說我怎么就攤上這事谷朝。” “怎么了武花?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵圆凰,是天一觀的道長。 經(jīng)常有香客問我体箕,道長专钉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任累铅,我火速辦了婚禮跃须,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘娃兽。我一直安慰自己菇民,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著玉雾,像睡著了一般翔试。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上复旬,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天垦缅,我揣著相機(jī)與錄音,去河邊找鬼驹碍。 笑死壁涎,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的志秃。 我是一名探鬼主播怔球,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浮还!你這毒婦竟也來了竟坛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤钧舌,失蹤者是張志新(化名)和其女友劉穎担汤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體洼冻,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡崭歧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了撞牢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片率碾。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖屋彪,靈堂內(nèi)的尸體忽然破棺而出所宰,到底是詐尸還是另有隱情,我是刑警寧澤撼班,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布歧匈,位于F島的核電站,受9級特大地震影響砰嘁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜勘究,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一矮湘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧口糕,春花似錦缅阳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秀撇。三九已至,卻和暖如春向族,著一層夾襖步出監(jiān)牢的瞬間呵燕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工件相, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留再扭,地道東北人。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓夜矗,卻偏偏與公主長得像泛范,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子紊撕,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容