原文鏈接: http://draveness.me/isa/
關(guān)注倉(cāng)庫(kù)隘截,及時(shí)獲得更新:iOS-Source-Code-Analyze
因?yàn)?ObjC 的 runtime 只能在 Mac OS 下才能編譯桶唐,所以文章中的代碼都是在 Mac OS椭赋,也就是
x86_64
架構(gòu)下運(yùn)行的,對(duì)于在 arm64 中運(yùn)行的代碼會(huì)特別說(shuō)明省撑。
如果你曾經(jīng)對(duì) ObjC 底層的實(shí)現(xiàn)有一定的了解魄衅,你應(yīng)該會(huì)知道 Objective-C 對(duì)象都是 C 語(yǔ)言結(jié)構(gòu)體鳍寂,所有的對(duì)象都包含一個(gè)類型為 isa
的指針蝗柔,那么你可能確實(shí)對(duì) ObjC 的底層有所知闻葵,不過(guò)現(xiàn)在的 ObjC 對(duì)象的結(jié)構(gòu)已經(jīng)不是這樣了。代替 isa
指針的是結(jié)構(gòu)體 isa_t
, 這個(gè)結(jié)構(gòu)體中"包含"了當(dāng)前對(duì)象指向的類的信息癣丧,這篇文章中會(huì)介紹一些關(guān)于這個(gè)變化的知識(shí)槽畔。
struct objc_object {
isa_t isa;
};
當(dāng) ObjC 為為一個(gè)對(duì)象分配內(nèi)存,初始化實(shí)例變量后胁编,在這些對(duì)象的實(shí)例變量的結(jié)構(gòu)體中的第一個(gè)就是 isa
厢钧。
![class-object-struct](http://7xrlu3.com1.z0.glb.clouddn.com/2016-04-21-NSObject + NSObject Copy + @Draveness.png)
所有繼承自
NSObject
的類實(shí)例化后的對(duì)象都會(huì)包含一個(gè)類型為isa_t
的結(jié)構(gòu)體。
從上圖中可以看出掏呼,不只是實(shí)例會(huì)包含一個(gè) isa
結(jié)構(gòu)體坏快,所有的類也有這么一個(gè) isa
铅檩。在 ObjC 中 Class 的定義也是一個(gè)名為 objc_class
的結(jié)構(gòu)體憎夷,如下:
struct objc_class : objc_object {
isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;
};
由于
objc_class
結(jié)構(gòu)體是繼承自objc_object
的,所以在這里顯式地寫出了isa_t isa
這個(gè)成員變量昧旨。
isa 指針的作用與元類
到這里拾给,我們就明白了:Objective-C 中類也是一個(gè)對(duì)象祥得。
這個(gè) isa
包含了什么呢?回答這個(gè)問(wèn)題之前蒋得,要引入了另一個(gè)概念 元類(meta class)级及,我們先了解一些關(guān)于元類的信息。
因?yàn)樵?Objective-C 中额衙,對(duì)象的方法并沒(méi)有存儲(chǔ)于對(duì)象的結(jié)構(gòu)體中(如果每一個(gè)對(duì)象都保存了自己能執(zhí)行的方法饮焦,那么對(duì)內(nèi)存的占用有極大的影響)。
當(dāng)實(shí)例方法被調(diào)用時(shí)窍侧,它要通過(guò)自己持有的 isa
來(lái)查找對(duì)應(yīng)的類县踢,然后在這里的 class_data_bits_t
結(jié)構(gòu)體中查找對(duì)應(yīng)方法的實(shí)現(xiàn)。同時(shí)伟件,每一個(gè) objc_class
也有一個(gè)指向自己的父類的指針 super_class
用來(lái)查找繼承的方法硼啤。
關(guān)于如何在
class_data_bits_t
中查找對(duì)應(yīng)方法會(huì)在之后的文章中講到。這里只需要知道斧账,它會(huì)在這個(gè)結(jié)構(gòu)體中查找到對(duì)應(yīng)方法的實(shí)現(xiàn)就可以了谴返。
![Slice 1](http://7xrlu3.com1.z0.glb.clouddn.com/2016-04-21-Slice 1.png)
但是,這樣就有一個(gè)問(wèn)題咧织,類方法的實(shí)現(xiàn)又是如何查找并且調(diào)用的呢嗓袱?這時(shí),就需要引入元類來(lái)保證無(wú)論是類還是對(duì)象都能通過(guò)相同的機(jī)制查找方法的實(shí)現(xiàn)习绢。
讓每一個(gè)類的 isa
指向?qū)?yīng)的元類索抓,這樣就達(dá)到了使類方法和實(shí)例方法的調(diào)用機(jī)制相同的目的:
- 實(shí)例方法調(diào)用時(shí),通過(guò)對(duì)象的
isa
在類中獲取方法的實(shí)現(xiàn) - 類方法調(diào)用時(shí)毯炮,通過(guò)類的
isa
在元類中獲取方法的實(shí)現(xiàn)
下面這張圖介紹了對(duì)象逼肯,類與元類之間的關(guān)系,筆者認(rèn)為已經(jīng)覺(jué)得足夠清晰了桃煎,所以不在贅述篮幢。
圖片來(lái)自 objc_explain_Classes_and_metaclasses
有關(guān)與介紹類與元類之間的關(guān)系的文章實(shí)在是太多了,因?yàn)檫@篇文章主要介紹 isa
为迈,在這一小節(jié)只是對(duì)其作用以及元類的概念進(jìn)行介紹三椿。如果想要了解更多關(guān)于類與元類的信息,可以看 What is a meta-class in Objective-C?
結(jié)構(gòu)體 isa_t
其實(shí) isa_t
是一個(gè)定義得非常"奇怪"的結(jié)構(gòu)體葫辐,在 ObjC 源代碼中可以看到這樣的定義:
#define ISA_MASK 0x00007ffffffffff8ULL
#define ISA_MAGIC_MASK 0x001f800000000001ULL
#define ISA_MAGIC_VALUE 0x001d800000000001ULL
#define RC_ONE (1ULL<<56)
#define RC_HALF (1ULL<<7)
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 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;
};
};
這是在
__x86_64__
上的實(shí)現(xiàn)搜锰,對(duì)于 iPhone5s 等架構(gòu)為__arm64__
的設(shè)備上,具體結(jié)構(gòu)體的實(shí)現(xiàn)和位數(shù)可能有些差別耿战,不過(guò)這些字段都是存在的蛋叼,可以看這里的 arm64 上結(jié)構(gòu)體的實(shí)現(xiàn)
在本篇文章中, 我們會(huì)以 __x86_64__
為例進(jìn)行分析,而不會(huì)對(duì)兩種架構(gòu)下由于不同的內(nèi)存布局方式導(dǎo)致的差異進(jìn)行分析。在我看來(lái)狈涮,這個(gè)細(xì)節(jié)不會(huì)影響對(duì) isa
指針的理解狐胎,不過(guò)還是要知道的。
筆者對(duì)這個(gè) isa_t
的實(shí)現(xiàn)聲明順序有一些更改歌馍,更方便分析和理解握巢。
union isa_t {
...
}?;
isa_t
是一個(gè) union
類型的結(jié)構(gòu)體,對(duì) union
不熟悉的讀者可以看這個(gè) stackoverflow 上的回答. 也就是說(shuō)其中的 isa_t
松却、cls
暴浦、 bits
還有結(jié)構(gòu)體共用同一塊地址空間。而 isa
總共會(huì)占據(jù) 64 位的內(nèi)存空間(決定于其中的結(jié)構(gòu)體)
struct {
uintptr_t indexed : 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;
};
isa 的初始化
我們可以通過(guò) isa
初始化的方法 initIsa
來(lái)初步了解這 64 位的 bits 的作用:
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
initIsa(cls, true, hasCxxDtor);
}
inline void
objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor)
{
if (!indexed) {
isa.cls = cls;
} else {
isa.bits = ISA_MAGIC_VALUE;
isa.has_cxx_dtor = hasCxxDtor;
isa.shiftcls = (uintptr_t)cls >> 3;
}
}
indexed 和 magic
當(dāng)我們對(duì)一個(gè) ObjC 對(duì)象分配內(nèi)存時(shí)晓锻,其方法調(diào)用棧中包含了上述的兩個(gè)方法肉渴,這里關(guān)注的重點(diǎn)是 initIsa
方法,由于在 initInstanceIsa
方法中傳入了 indexed = true
带射,所以同规,我們簡(jiǎn)化一下這個(gè)方法的實(shí)現(xiàn):
inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor)
{
isa.bits = ISA_MAGIC_VALUE;
isa.has_cxx_dtor = hasCxxDtor;
isa.shiftcls = (uintptr_t)cls >> 3;
}
對(duì)整個(gè) isa
的值 bits
進(jìn)行設(shè)置,傳入 ISA_MAGIC_VALUE
:
#define ISA_MAGIC_VALUE 0x001d800000000001ULL
我們可以把它轉(zhuǎn)換成二進(jìn)制的數(shù)據(jù)窟社,然后看一下哪些屬性對(duì)應(yīng)的位被這行代碼初始化了(標(biāo)記為紅色):
從圖中了解到券勺,在使用 ISA_MAGIC_VALUE
設(shè)置 isa_t
結(jié)構(gòu)體之后,實(shí)際上只是設(shè)置了 indexed
以及 magic
這兩部分的值灿里。
-
其中
indexed
表示isa_t
的類型-
0 表示
raw isa
关炼,也就是沒(méi)有結(jié)構(gòu)體的部分,訪問(wèn)對(duì)象的isa
會(huì)直接返回一個(gè)指向cls
的指針匣吊,也就是在 iPhone 遷移到 64 位系統(tǒng)之前時(shí) isa 的類型儒拂。union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; };
-
1 表示當(dāng)前
isa
不是指針,但是其中也有cls
的信息色鸳,只是其中關(guān)于類的指針都是保存在shiftcls
中社痛。union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; struct { uintptr_t indexed : 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; }; };
-
magic
的值為0x3b
用于調(diào)試器判斷當(dāng)前對(duì)象是真的對(duì)象還是沒(méi)有初始化的空間
has_cxx_dtor
在設(shè)置 indexed
和 magic
值之后避消,會(huì)設(shè)置 isa
的 has_cxx_dtor
葛碧,這一位表示當(dāng)前對(duì)象有 C++ 或者 ObjC 的析構(gòu)器(destructor),如果沒(méi)有析構(gòu)器就會(huì)快速釋放內(nèi)存拄踪。
isa.has_cxx_dtor = hasCxxDtor;
shiftcls
在為 indexed
吏砂、 magic
和 has_cxx_dtor
設(shè)置之后撵儿,我們就要將當(dāng)前對(duì)象對(duì)應(yīng)的類指針存入 isa
結(jié)構(gòu)體中了。
isa.shiftcls = (uintptr_t)cls >> 3;
將當(dāng)前地址右移三位的主要原因是用于將 Class 指針中無(wú)用的后三位清楚減小內(nèi)存的消耗狐血,因?yàn)轭惖闹羔樢凑兆止?jié)(8 bits)對(duì)齊內(nèi)存淀歇,其指針后三位都是沒(méi)有意義的 0。
絕大多數(shù)機(jī)器的架構(gòu)都是 byte-addressable 的匈织,但是對(duì)象的內(nèi)存地址必須對(duì)齊到字節(jié)的倍數(shù)浪默,這樣可以提高代碼運(yùn)行的性能,在 iPhone5s 中虛擬地址為 33 位,所以用于對(duì)齊的最后三位比特為
000
浴鸿,我們只會(huì)用其中的 30 位來(lái)表示對(duì)象的地址。
而 ObjC 中的類指針的地址后三位也為 0弦追,在 _class_createInstanceFromZone
方法中打印了調(diào)用這個(gè)方法傳入的類指針:
可以看到岳链,這里打印出來(lái)的所有類指針十六進(jìn)制地址的最后一位都為 8 或者 0。也就是說(shuō)劲件,類指針的后三位都為 0掸哑,所以,我們?cè)谏厦娲鎯?chǔ) Class
指針時(shí)右移三位是沒(méi)有問(wèn)題的零远。
isa.shiftcls = (uintptr_t)cls >> 3;
如果再嘗試打印對(duì)象指針的話苗分,會(huì)發(fā)現(xiàn)所有對(duì)象內(nèi)存地址的后四位都是 0,說(shuō)明 ObjC 在初始化內(nèi)存時(shí)是以 16 個(gè)字節(jié)對(duì)齊的, 分配的內(nèi)存地址后四位都是 0牵辣。
使用整個(gè)指針大小的內(nèi)存來(lái)存儲(chǔ)
isa
指針有些浪費(fèi)摔癣,尤其在 64 位的 CPU 上。在ARM64
運(yùn)行的 iOS 只使用了 33 位作為指針(與結(jié)構(gòu)體中的 33 位無(wú)關(guān)纬向,Mac OS 上為 47 位)择浊,而剩下的 31 位用于其它目的。類的指針也同樣根據(jù)字節(jié)對(duì)齊了逾条,每一個(gè)類指針的地址都能夠被 8 整除琢岩,也就是使最后 3 bits 為 0,為isa
留下 34 位用于性能的優(yōu)化师脂。Using an entire pointer-sized piece of memory for the isa pointer is a bit wasteful, especially on 64-bit CPUs which don't use all 64 bits of a pointer. ARM64 running iOS currently uses only 33 bits of a pointer, leaving 31 bits for other purposes. Class pointers are also aligned, meaning that a class pointer is guaranteed to be divisible by 8, which frees up another three bits, leaving 34 bits of the isa available for other uses. Apple's ARM64 runtime takes advantage of this for some great performance improvements.
from ARM64 and You
我嘗試運(yùn)行了下面的代碼將 NSObject
的類指針和對(duì)象的 isa
打印出來(lái)担孔,具體分析一下
object_pointer: 0000000001011101100000000000000100000000001110101110000011111001 // 補(bǔ)全至 64 位
class_pointer: 100000000001110101110000011111000
編譯器對(duì)直接訪問(wèn)
isa
的操作會(huì)有警告,因?yàn)橹苯釉L問(wèn)isa
已經(jīng)不會(huì)返回類指針了吃警,這種行為已經(jīng)被啟用了糕篇,取而代之的是使用 ISA() 方法來(lái)獲取類指針。
代碼中的 object
對(duì)象的 isa
結(jié)構(gòu)體中的內(nèi)容是這樣的:
其中紅色的為類指針酌心,與上面打印出的 [NSObject class]
指針右移三位的結(jié)果完全相同娩缰。這也就驗(yàn)證了我們之前對(duì)于初始化 isa
時(shí)對(duì) initIsa
方法的分析是正確的。它設(shè)置了 indexed
谒府、magic
以及 shiftcls
拼坎。
<a id='ISA()'></a>ISA() 方法
因?yàn)槲覀兪褂媒Y(jié)構(gòu)體取代了原有的 isa 指針,所以要提供一個(gè)方法 ISA()
來(lái)返回類指針完疫。
其中 ISA_MASK
是宏定義泰鸡,這里通過(guò)掩碼的方式獲取類指針:
#define ISA_MASK 0x00007ffffffffff8ULL
inline Class
objc_object::ISA()
{
return (Class)(isa.bits & ISA_MASK);
}
其它 bits
在 isa_t
中,我們還有一些沒(méi)有介紹的其它 bits壳鹤,在這個(gè)小結(jié)就簡(jiǎn)單介紹下這些 bits 的作用
-
has_assoc
- 對(duì)象含有或者曾經(jīng)含有關(guān)聯(lián)引用盛龄,沒(méi)有關(guān)聯(lián)引用的可以更快地釋放內(nèi)存
-
weakly_referenced
- 對(duì)象被指向或者曾經(jīng)指向一個(gè) ARC 的弱變量,沒(méi)有弱引用的對(duì)象可以更快釋放
-
deallocating
- 對(duì)象正在釋放內(nèi)存
-
has_sidetable_rc
- 對(duì)象的引用計(jì)數(shù)太大了,存不下
-
extra_rc
- 對(duì)象的引用計(jì)數(shù)超過(guò) 1余舶,會(huì)存在這個(gè)這個(gè)里面啊鸭,如果引用計(jì)數(shù)為 10,
extra_rc
的值就為 9
- 對(duì)象的引用計(jì)數(shù)超過(guò) 1余舶,會(huì)存在這個(gè)這個(gè)里面啊鸭,如果引用計(jì)數(shù)為 10,
struct {
uintptr_t indexed : 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;
};
<a id='arm64'></a>arm64 架構(gòu)中的 isa_t
結(jié)構(gòu)體
#define ISA_MASK 0x0000000ffffffff8ULL
#define ISA_MAGIC_MASK 0x000003f000000001ULL
#define ISA_MAGIC_VALUE 0x000001a000000001ULL
#define RC_ONE (1ULL<<45)
#define RC_HALF (1ULL<<18)
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
參考資料
- Objective-C Runtime Programming Guide
- What is a meta-class in Objective-C?
- objc_explain_Classes_and_metaclasses
- Storing things in isa
- Why do we need C Unions?
- objc_explain_Non-pointer_isa
- Tagged Pointer
- ARM64 and You
- 64位與Tagged Pointer
關(guān)注倉(cāng)庫(kù)匿值,及時(shí)獲得更新:iOS-Source-Code-Analyze
轉(zhuǎn)載請(qǐng)注明 Blog: Draveness
Follow: @Draveness·Github