Runtime源碼 —— 對象乖寒、類和isa

猶記得當(dāng)初學(xué)習(xí)C++的時候,買過一本侯捷老師的《STL源碼剖析》院溺,書里的內(nèi)容基本沒看楣嘁,就記得最前面有句話:

源碼面前,了無秘密

類珍逸、對象逐虚、方法和屬性算是寫OC代碼時接觸的最多的部分了。本篇就以對象為切入點谆膳,分析一下對象和類在runtime層面的表示叭爱。

對象

繼承于NSObject的類所生成的對象在runtime中的表示是這樣的:

struct objc_object {
    isa_t isa;
}

很簡單,就一個isa_t結(jié)構(gòu)體漱病,從名字也可以看出來這個結(jié)構(gòu)體指明了這個對象是什么买雾,也就是所屬的類,isa_t結(jié)構(gòu)體的定義如下:

union isa_t {
    Class cls;
    ...
}
(當(dāng)然不止這么點內(nèi)容杨帽,后面會詳細的分析)

可以看到這個結(jié)構(gòu)體中有個類型是Class的屬性cls漓穿,看起來里面應(yīng)該存有關(guān)于這個對象的類的相關(guān)信息,看看Class是如何定義的注盈。

typedef struct objc_class *Class;
struct objc_class : objc_object {
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

Class就是結(jié)構(gòu)體objc_class晃危,但是objc_class繼承于objc_object,那就是說類其實也是一個對象老客,只不過比通常我們理解的對象多了一些屬性山害,比如superclass等纠俭。

關(guān)于其他屬性的分析不是本文重點沿量,會在后續(xù)文章中結(jié)合方法(method)的實現(xiàn)進行分析浪慌。

先不看這些屬性,這里還有一個很奇怪的問題朴则,既然類也是一個objc_object权纤,那就是說類也有一個isa指針,那類的isa指針指向哪里呢乌妒?查看了不少資料汹想,這篇講的挺好:classes and metaclasses

大致的意思是在class之上撤蚊,還有叫做元類(meta class)的存在古掏,而class的isa指針就是指向?qū)?yīng)的meta class。

我們都知道class中存儲的是描述對象的相關(guān)信息侦啸,那么相應(yīng)的meta class中存放的就是描述class相關(guān)信息槽唾。說的更直白一點,在我們寫代碼時光涂,通過對象來調(diào)用的方法(實例方法)都是存儲在class中的庞萍,通過類名來調(diào)用的方法(類方法)都是存儲在meta class中的。

到這里對象和類的關(guān)系已經(jīng)比較清楚了忘闻,但是如果細細思考一下钝计,會發(fā)現(xiàn)還有一個問題,就是meta class也是有isa指針的齐佳,那么這個isa又指向了哪里呢私恬?在上面給出的那篇文章里面有這么一張圖:


class diagram.jpeg

這張圖解釋的非常清楚,meta class的isa指向了root meta class(絕大部分情況下root class就是NSObject)炼吴,root meta class的isa指向自身本鸣,isa的鏈路就是這樣了。

isa_t結(jié)構(gòu)體分析

先看看isa_t的完全版缺厉,因為運行環(huán)境是osx永高,所以只截取x86_64部分,arm64的區(qū)別只在于部分字段的位數(shù)不同提针,字段是完全相同的:

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

    Class cls;
    uintptr_t bits;

#   __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; 
        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)
    };
}

看這個定義只能大概看出個框架命爬,下面從isa的初始化過程來看看isa_t究竟是如何存儲類或者元類的相關(guān)信息。

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    initIsa(cls, true, hasCxxDtor);
}

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        isa_t newisa(0);
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
        isa = newisa;
    }
}

上來就看不懂辐脖,nonpointer是個什么饲宛,為什么在這里傳的是true?在之前那位大神的另一篇文章中也有解釋:Non-pointer isa嗜价。

大概的意思是在64位系統(tǒng)中艇抠,為了降低內(nèi)存使用幕庐,提升性能,isa中有一部分字段用來存儲其他信息家淤。這也解釋了上面isa_t的那部分結(jié)構(gòu)體异剥。

這有點像taggedPointer,兩者之間有什么區(qū)別絮重?備注一下后面再研究冤寿。

現(xiàn)在知道了nonpointer為什么是true,那么把initIsa方法先簡化一下:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    isa_t newisa(0);
    newisa.bits = ISA_MAGIC_VALUE;
    newisa.has_cxx_dtor = hasCxxDtor;
    newisa.shiftcls = (uintptr_t)cls >> 3;
    isa = newisa;
}

#   define ISA_MAGIC_VALUE 0x001d800000000001ULL

一共三部分:

  1. newisa.bits = ISA_MAGIC_VALUE;
    從ISA_MAGIC_VALUE的定義中可以看到這個字段初始化了兩個部分青伤,一個是magic字段(6位:111011)督怜,一個是nonpointer字段(1位:1),magic字段用于校驗狠角,nonpointer之前已經(jīng)詳細分析過了号杠。
  2. newisa.has_cxx_dtor = hasCxxDtor;
    這個字段存儲類是否有c++析構(gòu)器。
  3. newisa.shiftcls = (uintptr_t)cls >> 3;
    將cls右移3位存到shiftcls中丰歌,從isa_t的結(jié)構(gòu)體中也可以看到低3位都是用來存儲其他信息的姨蟋,既然可以右移三位,那就代表類地址的低三位全部都是0动遭,否則就出錯了芬探,補0的作用應(yīng)該是為了字節(jié)對齊。

因為nonpointer的緣故厘惦,isa并不只是用來存儲類地址了偷仿,所以需要提供一個額外的方法來返回真正的地址:

inline Class 
objc_object::ISA() 
{
    return (Class)(isa.bits & ISA_MASK);
}

#   define ISA_MASK        0x00007ffffffffff8ULL

其實就是取isa_t結(jié)構(gòu)體的shiftcls字段。

其他字段

還有一些其他的字段宵蕉,把上面那篇文章中相關(guān)部分翻譯過來放在下面:

// 是否曾經(jīng)或正在被關(guān)聯(lián)引用酝静,如果沒有,可以快速釋放內(nèi)存
uintptr_t has_assoc : 1;

// 對象是否曾經(jīng)或正在被弱引用羡玛,如果沒有别智,可以快速釋放內(nèi)存
uintptr_t weakly_referenced : 1;

// 對象是否正在釋放內(nèi)存
uintptr_t deallocating : 1;

// 對象的引用計數(shù)太大,無法存儲
uintptr_t has_sidetable_rc : 1;

// 對象的引用計數(shù)超過1稼稿,比如10薄榛,則此值為9
uintptr_t extra_rc : 8;

例子

下面通過代碼驗證一下之前關(guān)于isa的鏈路,先創(chuàng)建一個用于測試TestObject類让歼,相關(guān)代碼如下:

// TestObject.h
#import <Foundation/Foundation.h>
@interface TestObject : NSObject

@end

// TestObject.m
#import "TestObject.h"
@implementation TestObject

@end

為了方便打條件斷點敞恋,先通過log獲取TestObject在內(nèi)存中的位置:0x100001180,這個時候main函數(shù)是這個樣子的:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TestObject *testObj = [TestObject new];
        NSLog(@"%p", [testObj class]);
        NSLog(@"%p", [TestObject class]);
        NSLog(@"%p", [NSObject class]);
    }
    return 0;
}

只要代碼不變谋右,這個類在內(nèi)存中的地址就不會變

所以在initIsa()方法中添加一個條件斷點硬猫,并重新運行:


1.png

運行程序,當(dāng)進入斷點的時候可以看到方法的調(diào)用棧是這樣的:


2.png

找到2 _class_createInstanceFromZone(),在方法最后打個斷點啸蜜,繼續(xù)運行程序進入此斷點坑雅,輸出obj的內(nèi)存地址:
Printing description of obj:
<TestObject: 0x101301090>

接下來通過這個地址來測試一下isa的鏈路:

//方法中的obj類型是id,id就是objc_object*衬横,所以強轉(zhuǎn)一下
(lldb) p (objc_object *)0x101301090
(objc_object *) $3 = 0x0000000101301090
(lldb) p $3->isa 
(objc_class *) $4 = 0x001d800100001181 // 對象的isa
(lldb) p (objc_object *)0x100001180 // 根據(jù)上面isa_t結(jié)構(gòu)體裹粤,找到shiftcls的地址,也就是類的真實地址
(objc_object *) $5 = 0x0000000100001180 // TestObject類的真實地址冕香,可以看到與之前打印的[TestObject class]地址是相同的
(lldb) p $5->isa
(objc_class *) $6 = 0x001d800100001159 // 類的isa
(lldb) p (objc_object *)0x100001158
(objc_object *) $7 = 0x0000000100001158 // TestObject元類的真實地址
(lldb) p $7->isa
(objc_class *) $8 = 0x001d8001004a0e49 // 根元類的isa
(lldb) p (objc_object *)0x1004a0e48
(objc_object *) $9 = 0x00000001004a0e48
(lldb) p $9->isa
(objc_class *) $10 = 0x001d8001004a0e49 // 可以看到根元類的isa確實指向自身
(lldb) 

測試結(jié)果與圖class diagram.jpeg給出的完全相同蛹尝。

在main函數(shù)的return行添加斷點,運行程序進入斷點悉尾,有如下輸出:

NSLog(@"%p", [testObj class]); 
NSLog(@"%p", [TestObject class]);
NSLog(@"%p", [NSObject class]); 

log輸出:
0x100001180
0x100001180
0x1004a0e98

前兩個log結(jié)果相同,稍后再分析挫酿,這里先看一下NSObject的元類isa:

(lldb) p (objc_object *)0x1004a0e98
(objc_object *) $11 = 0x00000001004a0e98
(lldb) p $11->isa
(objc_class *) $12 = 0x001d8001004a0e49

可以看到此處$12的值與上方$10是完全相同的构眯,也就驗證了NSObject的meta class就是一般類的root meta class。

下面再來看看上面那兩個相同的輸出早龟,也就是

TestObject *testObj = [TestObject new];
NSLog(@"%d", [testObj class] == [TestObject class]);

這個log會輸出1

看起來有點奇怪惫霸,但是只要看一下源代碼實現(xiàn)就能理解了。

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

object_getClass方法最終返回的是isa葱弟。所以TestObject調(diào)用class方法壹店,返回的是自身;testObj調(diào)用class方法芝加,返回的是isa指向的類硅卢,也是TestObject。所以上面結(jié)果相同就不奇怪了藏杖。

看到這里就順便看一下可能會接觸到isa的常用的幾個方法:isMemberOfClass将塑,isKindOfClass。廢話不說蝌麸,直接上源碼:

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

從源碼來看這兩個方法就一目了然了点寥。有興趣的可以寫幾個例子測試一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末来吩,一起剝皮案震驚了整個濱河市敢辩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌弟疆,老刑警劉巖戚长,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異兽间,居然都是意外死亡历葛,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恤溶,“玉大人乓诽,你說我怎么就攤上這事≈涑蹋” “怎么了鸠天?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長帐姻。 經(jīng)常有香客問我稠集,道長,這世上最難降的妖魔是什么饥瓷? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任剥纷,我火速辦了婚禮,結(jié)果婚禮上呢铆,老公的妹妹穿的比我還像新娘晦鞋。我一直安慰自己,他們只是感情好棺克,可當(dāng)我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布悠垛。 她就那樣靜靜地躺著,像睡著了一般娜谊。 火紅的嫁衣襯著肌膚如雪确买。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天纱皆,我揣著相機與錄音湾趾,去河邊找鬼。 笑死抹剩,一個胖子當(dāng)著我的面吹牛撑帖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播澳眷,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼胡嘿,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了钳踊?” 一聲冷哼從身側(cè)響起衷敌,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拓瞪,沒想到半個月后缴罗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡祭埂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年面氓,在試婚紗的時候發(fā)現(xiàn)自己被綠了兵钮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡舌界,死狀恐怖掘譬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呻拌,我是刑警寧澤葱轩,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站藐握,受9級特大地震影響愿阐,放射性物質(zhì)發(fā)生泄漏袜蚕。R本人自食惡果不足惜又憨,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一锰扶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧抬闷,春花似錦妇蛀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽眷茁。三九已至炕泳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間上祈,已是汗流浹背培遵。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留登刺,地道東北人籽腕。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像纸俭,于是被迫代替她去往敵國和親皇耗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,455評論 2 359

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