探索底層原理托享,積累從點(diǎn)滴做起骚烧。大家好,我是Mars闰围。
往期回顧
iOS底層原理探索—OC對象的本質(zhì)
iOS底層原理探索—class的本質(zhì)
iOS底層原理探索—KVO的本質(zhì)
iOS底層原理探索— KVC的本質(zhì)
iOS底層原理探索— Category的本質(zhì)(一)
iOS底層原理探索— Category的本質(zhì)(二)
iOS底層原理探索— 關(guān)聯(lián)對象的本質(zhì)
iOS底層原理探索— block的本質(zhì)(一)
iOS底層原理探索— block的本質(zhì)(二)
今天繼續(xù)帶領(lǐng)大家探索iOS之Runtime
的本質(zhì)赃绊。
前言
OC是一門動(dòng)態(tài)性比較強(qiáng)的編程語言,它的動(dòng)態(tài)性是基于Runtime
的API
羡榴。Runtime
在我們的實(shí)際開發(fā)中占據(jù)著重要的地位碧查,在面試過程中也經(jīng)常遇到Runtime
相關(guān)的面試題,我們在之前幾期的探索分析時(shí)也經(jīng)常會(huì)到Runtime
的底層源碼中查看相關(guān)實(shí)現(xiàn)校仑。Runtime
對于iOS
開發(fā)者的重要性不言而喻忠售,想要學(xué)習(xí)和掌握Runtime
的相關(guān)技術(shù),就要從Runtime
底層的一些常用數(shù)據(jù)結(jié)構(gòu)入手迄沫。掌握了它的底層結(jié)構(gòu)稻扬,我們學(xué)習(xí)起來也能達(dá)到事半功倍的效果。今天先學(xué)習(xí)isa
羊瘩。
isa
我們在iOS底層原理探索—OC對象的本質(zhì)一文中講解OC對象本質(zhì)的時(shí)候提到泰佳,每個(gè)OC對象的底層結(jié)構(gòu)體中都包含一個(gè)isa
指針:
struct NSObject_IMPL {
Class isa;
};
在arm64
架構(gòu)之前,isa
僅是一個(gè)指針尘吗,保存著類對象(Class)或元類對象(Meta-Class)的內(nèi)存地址乐纸,在arm64
架構(gòu)之后,蘋果對isa
進(jìn)行了優(yōu)化摇予,變成了一個(gè)isa_t
類型的共用體(union)結(jié)構(gòu)汽绢,同時(shí)使用位域來存儲更多的信息:
也就是說,我們之前熟知的OC對象的
isa
指針并不是直接指向類對象或者元類對象的內(nèi)存地址侧戴,而是需要&ISA_MASK
通過位運(yùn)算才能獲取到類對象或者元類對象的地址宁昭。
現(xiàn)在大家可能心存疑問跌宛,什么是共用體?什么是位域积仗?位運(yùn)算又是什么疆拘?不要著急,接下來一一為大家解答寂曹。
1哎迄、位域
位域是指信息在存儲時(shí),并不需要占用一個(gè)完整的字節(jié)隆圆, 而只需占一個(gè)或幾個(gè)二進(jìn)制位漱挚。例如生活中的電燈開關(guān),它只有“開”渺氧、“關(guān)”兩種狀態(tài)旨涝,那我們就可以用 1
和 0
來分別代表這兩種狀態(tài),這樣我們就僅僅用了一個(gè)二進(jìn)制位就保存了開關(guān)的狀態(tài)侣背。這樣一來不僅節(jié)省存儲空間白华,還使處理更加簡便。
2贩耐、位運(yùn)算符
在計(jì)算機(jī)語言中弧腥,除了加、減潮太、乘鸟赫、除等這樣的算術(shù)運(yùn)算符之外還有很多運(yùn)算符,這里只為大家簡單講解一下位運(yùn)算符消别。
位運(yùn)算符用來對二進(jìn)制位進(jìn)行操作抛蚤,當(dāng)然,操作數(shù)只能為整型和字符型數(shù)據(jù)寻狂。C
語言中六種位運(yùn)算符: &
按位與岁经、 |
按位或、 ^
按位異或蛇券、 ~
非缀壤、 <<
左移和 >>
右移。
我們依舊引用上面的電燈開關(guān)論纠亚,只不過現(xiàn)在我們有兩個(gè)開關(guān)塘慕,1
代表開,0
代表關(guān)蒂胞。
1) 按位與 &
有0出0图呢,全1為1。
我們可以理解為在按位與運(yùn)算中,兩個(gè)開關(guān)是串聯(lián)的蛤织,如果我們想要燈亮赴叹,需要兩個(gè)開關(guān)都打開燈才會(huì)亮,所以是
1 & 1 = 1
指蚜。如果任意一個(gè)開關(guān)沒打開乞巧,燈都不會(huì)亮,所以其他運(yùn)算都是0摊鸡。
2) 按位或 |
有1出1绽媒,全0出0。
在按位或運(yùn)算中免猾,我們可以理解為兩個(gè)開關(guān)是并聯(lián)的是辕,即一個(gè)開關(guān)開,燈就會(huì)亮掸刊。只有當(dāng)兩個(gè)開關(guān)都是關(guān)的,燈才不會(huì)亮赢乓。
3) 按位異或 ^
相同為0忧侧,不同為1。
4) 非 ~
非運(yùn)算即取反運(yùn)算牌芋,在二進(jìn)制中 1 變 0 蚓炬,0 變 1。例如110101
進(jìn)行非運(yùn)算后為001010
躺屁,即1010
肯夏。
5) 左移 <<
左移運(yùn)算就是把<<
左邊的運(yùn)算數(shù)的各二進(jìn)位全部左移若干位,移動(dòng)的位數(shù)即<<
右邊的數(shù)的數(shù)值犀暑,高位丟棄驯击,低位補(bǔ)0。
左移n位就是乘以2的n次方耐亏。例如:a<<4
是指把a(bǔ)的各二進(jìn)位向左移動(dòng)4位徊都。如a=00000011(十進(jìn)制3),左移4位后為00110000(十進(jìn)制48)广辰。
6) 右移 >>
右移運(yùn)算就是把>>
左邊的運(yùn)算數(shù)的各二進(jìn)位全部右移若干位暇矫,>>
右邊的數(shù)指定移動(dòng)的位數(shù)。例如:設(shè) a=15择吊,a>>2 表示把00001111右移為00000011(十進(jìn)制3)李根。
簡單了解了位運(yùn)算符后,下面為大家介紹位運(yùn)算符的兩種運(yùn)用場景几睛。
位運(yùn)算符的運(yùn)用
1房轿、取值
可以利用按位與 &
運(yùn)算取出指定位的值,具體操作是想取出哪一位的值就將那一位置為1,其它位都為0冀续,然后同原數(shù)據(jù)進(jìn)行按位與計(jì)算琼讽,即可取出特定的位。
例:
0000 0011
取出倒數(shù)第三位的值
// 想取出倒數(shù)第三位的值洪唐,就將倒數(shù)第三位的值置為1钻蹬,其它位為0,跟原數(shù)據(jù)按位與運(yùn)算
0000 0011
& 0000 0100
------------
0000 0000 // 得出按位與運(yùn)算后的結(jié)果凭需,即可拿到原數(shù)據(jù)中倒數(shù)第三位的值為0
上面的例子中问欠,我們從0000 0011
中取值,則0000 0011
被稱之為源碼粒蜈。進(jìn)行按位與操作設(shè)定的0000 0100
稱之為掩碼
2顺献、設(shè)值
可以通過按位或 |
運(yùn)算符將某一位的值設(shè)為1或0。具體操作是:
想將某一位的值置為1的話枯怖,那么就將掩碼中對應(yīng)位的值設(shè)為1注整,掩碼其它位為0,將源碼與掩碼進(jìn)行按位或操作即可度硝。
例:將
0000 0011
倒數(shù)第三位的值改為1
// 改變倒數(shù)第三位的值肿轨,就將掩碼倒數(shù)第三位的值置為1,其它位為0蕊程,跟源碼按位或運(yùn)算
0000 0011
| 0000 0100
------------
0000 0111 // 即可將源碼中倒數(shù)第三位的值改為1
想將某一位的值置為0的話椒袍,那么就將掩碼中對應(yīng)位的值設(shè)為0,掩碼其它位為1藻茂,將源碼與掩碼進(jìn)行按位或操作即可驹暑。
例:將
0000 0011
倒數(shù)第二位的值改為0
// 改變倒數(shù)第二位的值,就將掩碼倒數(shù)第二位的值置為0辨赐,其它位為1优俘,跟源碼按位或運(yùn)算
0000 0011
| 1111 1101
------------
0000 0001 // 即可將源碼中倒數(shù)第二位的值改為0
到這里相信大家對位運(yùn)算符有了一定的了解,下面我們通過OC代碼的一個(gè)例子掀序,來將位運(yùn)算符運(yùn)用到實(shí)際代碼開發(fā)中兼吓。
我們聲明一個(gè)Man
類,類中有三個(gè)BOOL
類型的屬性森枪,分別為tall
视搏、rich
、handsome
县袱,通過這三個(gè)屬性來判斷這個(gè)人是否高富帥浑娜。
然后我們查看一下一個(gè)
Man
類對象所占據(jù)的內(nèi)存大小:我們看到式散,一個(gè)
Man
類的對象占16個(gè)字節(jié)筋遭。其中包括一個(gè)isa
指針和三個(gè)BOOL
類型的屬性,8+1+1+1=11,根據(jù)內(nèi)存對齊原則所以一個(gè)Man
類的對象占16個(gè)字節(jié)漓滔。
我們知道编饺,BOOL
值只有兩種情況:0
或 1
,占據(jù)一個(gè)字節(jié)的內(nèi)存空間响驴。而一個(gè)字節(jié)的內(nèi)存空間中又有8個(gè)二進(jìn)制位透且,并且二進(jìn)制同樣只有 0
或 1
,那么我們完全可以使用1個(gè)二進(jìn)制位來表示一個(gè)BOOL
值豁鲤。也就是說我們上面聲明的三個(gè)BOOL
值最終只使用3個(gè)二進(jìn)制位就可以秽誊,這樣就節(jié)省了內(nèi)存空間。那我們?nèi)绾螌?shí)現(xiàn)呢琳骡?
想要實(shí)現(xiàn)三個(gè)BOOL
值存放在一個(gè)字節(jié)中锅论,我們可以通過char
類型的成員變量來實(shí)現(xiàn)。char
類型占一個(gè)字節(jié)內(nèi)存空間楣号,也就是8個(gè)二進(jìn)制位最易。可以使用其中最后三個(gè)二進(jìn)制位來存儲3個(gè)BOOL
值炫狱。
當(dāng)然我們不能把char
類型寫成屬性藻懒,因?yàn)橐坏懗蓪傩裕到y(tǒng)會(huì)自動(dòng)幫我們添加成員變量毕荐,自動(dòng)實(shí)現(xiàn)set
和get
方法束析。
@interface Man()
{
char _tallRichHandsome;
}
如果我們賦值_tallRichHansome
為1
艳馒,即0b 0000 0001
憎亚,只使用8個(gè)二進(jìn)制位中的最后3個(gè)分別用0
或者1
來代表tall
、rich
弄慰、handsome
的值第美。那么此時(shí)tall
、rich
陆爽、handsome
的狀態(tài)為:
結(jié)合我們上文將的6中位運(yùn)算符以及使用場景什往,我們可以分別聲明
tall
、rich
慌闭、handsome
的掩碼别威,來方便我們進(jìn)行下一步的位運(yùn)算取值和賦值:
#define Tall_Mask 0b00000100 //此二進(jìn)制數(shù)對應(yīng)十進(jìn)制數(shù)為 4
#define Rich_Mask 0b00000010 //此二進(jìn)制數(shù)對應(yīng)十進(jìn)制數(shù)為 2
#define Handsome_Mask 0b00000001 //此二進(jìn)制數(shù)對應(yīng)十進(jìn)制數(shù)為 1
通過對位運(yùn)算符的左移 <<
和右移 >>
的了解,我們可以將上面的代碼優(yōu)化成:
#define Tall_Mask (1<<2) // 0b00000100
#define Rich_Mask (1<<1) // 0b00000010
#define Handsome_Mask (1<<0) // 0b00000001
自定義的set
方法如下:
- (void)setTall:(BOOL)tall
{
if (tall) { // 如果需要將值置為1驴剔,將源碼和掩碼進(jìn)行按位或運(yùn)算
_tallRichHandsome |= Tall_Mask;
}else{ // 如果需要將值置為0 // 將源碼和按位取反后的掩碼進(jìn)行按位與運(yùn)算
_tallRichHandsome &= ~Tall_Mask;
}
}
- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHandsome |= Rich_Mask;
}else{
_tallRichHandsome &= ~Rich_Mask;
}
}
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHandsome |= Handsome_Mask;
}else{
_tallRichHandsome &= ~Handsome_Mask;
}
}
自定義的get
方法如下:
- (BOOL)isTall
{
return !!(_tallRichHandsome & Tall_Mask);
}
- (BOOL)isRich
{
return !!(_tallRichHandsome & Rich_Mask);
}
- (BOOL)isHandsome
{
return !!(_tallRichHandsome & Handsome_Mask);
}
此處需要注意的是省古,代碼中!
為邏輯運(yùn)算符非
,因?yàn)?code>_tallRichHandsome & Tall_Mask代碼執(zhí)行后丧失,返回的肯定是一個(gè)整型數(shù)豺妓,如當(dāng)tall
為YES
時(shí),說明二進(jìn)制數(shù)為0b 0000 0100
,對應(yīng)的十進(jìn)制數(shù)為4琳拭,那么進(jìn)行一次邏輯非運(yùn)算后训堆,!(4)
的值為0
,對0
再進(jìn)行一次邏輯非運(yùn)算!(0)
白嘁,結(jié)果就成了1
坑鱼,那么正好跟tall
為YES
對應(yīng)。所以此處進(jìn)行兩次邏輯非運(yùn)算权薯,!!
姑躲。
當(dāng)然,還要實(shí)現(xiàn)初始化方法:
- (instancetype)init
{
if (self = [super init]) {
_tallRichHandsome = 0b00000100;
}
return self;
}
通過測試驗(yàn)證盟蚣,我們完成了取值和賦值:
使用結(jié)構(gòu)體位域優(yōu)化代碼
我們上文講到了位域
的概念黍析,那么我們就可以使用結(jié)構(gòu)體位域
來優(yōu)化一下我們的代碼。這樣就不用再額外聲明上面代碼中的掩碼部分屎开。位域聲明的格式是位域名 : 位域長度
阐枣。
在使用位域
的過程中需要注意以下幾點(diǎn):
1.如果一個(gè)字節(jié)所剩空間不夠存放另一位域時(shí)奄抽,應(yīng)從下一單元起存放該位域蔼两。
2.位域的長度不能大于數(shù)據(jù)類型本身的長度,比如int
類型就不能超過32位二進(jìn)位逞度。
3.位域可以無位域名额划,這時(shí)它只用來作填充或調(diào)整位置。無名的位域是不能使用的档泽。
使用位域優(yōu)化以后:
測試看一下是否正確俊戳,這次我們將
tall
設(shè)為YES
、rich
設(shè)為NO
馆匿、handsome
設(shè)為YES
:依舊完成賦值和取值抑胎。
但是代碼這樣優(yōu)化后我們?nèi)サ袅搜诖a和初始化的代碼,可讀性很差渐北,我們繼續(xù)使用共用體進(jìn)行優(yōu)化:
使用共用體優(yōu)化代碼
我們可以使用比較高效的位運(yùn)算來進(jìn)行賦值和取值阿逃,使用union
共用體來對數(shù)據(jù)進(jìn)行存儲。這樣不僅可以增加讀取效率赃蛛,還可以增強(qiáng)代碼可讀性恃锉。
#define Tall_Mask (1<<2) // 0b00000100
#define Rich_Mask (1<<1) // 0b00000010
#define Handsome_Mask (1<<0) // 0b00000001
@interface Man()
{
union {
char bits;
// 結(jié)構(gòu)體僅僅是為了增強(qiáng)代碼可讀性
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
}_tallRichHandsome;
}
@end
@implementation Man
- (void)setTall:(BOOL)tall
{
if (tall) {
_tallRichHandsome.bits |= Tall_Mask;
}else{
_tallRichHandsome.bits &= ~Tall_Mask;
}
}
- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHandsome.bits |= Rich_Mask;
}else{
_tallRichHandsome.bits &= ~Rich_Mask;
}
}
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHandsome.bits |= Handsome_Mask;
}else{
_tallRichHandsome.bits &= ~Handsome_Mask;
}
}
- (BOOL)isTall
{
return !!(_tallRichHandsome.bits & Tall_Mask);
}
- (BOOL)isRich
{
return !!(_tallRichHandsome.bits & Rich_Mask);
}
- (BOOL)isHandsome
{
return !!(_tallRichHandsome.bits & Handsome_Mask);
}
其中_tallRichHandsome
共用體只占用一個(gè)字節(jié),因?yàn)榻Y(jié)構(gòu)體中tall
呕臂、rich
破托、handsome
都只占一位二進(jìn)制空間,所以結(jié)構(gòu)體只占一個(gè)字節(jié)诵闭,而char
類型的bits
也只占一個(gè)字節(jié)炼团,他們都在共用體中澎嚣,因此共用一個(gè)字節(jié)的內(nèi)存即可。
而且我們在set
瘟芝、get
方法中的賦值和取值通過使用掩碼進(jìn)行位運(yùn)算來增加效率易桃,整體邏輯也就很清晰了。
但是锌俱,如果我們在日常開發(fā)中這樣寫代碼的話晤郑,很可能會(huì)被同事打死。雖然代碼已經(jīng)很清晰了贸宏,但是整體閱讀起來很是很吃力的造寝。我們在這里學(xué)習(xí)位運(yùn)算以及共用體這些知識,更多的是為了方便我們閱讀OC底層的代碼吭练。下面我們就回到本文主題诫龙,查看一下isa_t
共用體的源碼。
isa_t共用體
我們發(fā)現(xiàn)在
isa_t
共用體內(nèi)用宏ISA_BITFIELD
定義了位域鲫咽,我們進(jìn)入位域內(nèi)查看源碼:我們看到屹篓,在內(nèi)部分別定義了
arm64
位架構(gòu)和x86_64
架構(gòu)的掩碼和位域肚医。我們只分析arm64
位架構(gòu)下的部分內(nèi)容(紅色標(biāo)注部分)轿腺。可以清楚看到
ISA_BITFIELD
位域的內(nèi)容以及掩碼ISA_MASK
的值:0x0000000ffffffff8ULL
梗劫。我們重點(diǎn)看一下
uintptr_t shiftcls : 33;
,在shiftcls
中存儲著類對象和元類對象的內(nèi)存地址信息箩绍,我們上文講到孔庭,對象的isa
指針需要同ISA_MASK
經(jīng)過一次按位與運(yùn)算才能得出真正的類對象地址。那么我們將ISA_MASK
的值0x0000000ffffffff8ULL
轉(zhuǎn)化為二進(jìn)制數(shù)分析一下:從圖中可以看到
ISA_MASK
的值轉(zhuǎn)化為二進(jìn)制中有33位都為1
材蛛,上文講到按位與運(yùn)算是可以取出這33位中的值圆到。那么就說明同ISA_MASK
進(jìn)行按位與運(yùn)算就可以取出類對象和元類對象的內(nèi)存地址信息。
我們繼續(xù)分析一下結(jié)構(gòu)體位域中其他的內(nèi)容代表的含義:
struct {
// 0代表普通的指針仰税,存儲著類對象构资、元類對象的內(nèi)存地址抽诉。
// 1代表優(yōu)化后的使用位域存儲更多的信息陨簇。
uintptr_t nonpointer : 1;
// 是否有設(shè)置過關(guān)聯(lián)對象,如果沒有迹淌,釋放時(shí)會(huì)更快
uintptr_t has_assoc : 1;
// 是否有C++析構(gòu)函數(shù)河绽,如果沒有,釋放時(shí)會(huì)更快
uintptr_t has_cxx_dtor : 1;
// 存儲著類對象唉窃、元類對象對象的內(nèi)存地址信息
uintptr_t shiftcls : 33;
// 用于在調(diào)試時(shí)分辨對象是否未完成初始化
uintptr_t magic : 6;
// 是否有被弱引用指向過耙饰。
uintptr_t weakly_referenced : 1;
// 對象是否正在釋放
uintptr_t deallocating : 1;
// 引用計(jì)數(shù)器是否過大無法存儲在isa中
// 如果為1,那么引用計(jì)數(shù)會(huì)存儲在一個(gè)叫SideTable的類的屬性中
uintptr_t has_sidetable_rc : 1;
// 里面存儲的值是引用計(jì)數(shù)器減1
uintptr_t extra_rc : 19;
};
至此我們已經(jīng)對isa
指針有了新的認(rèn)識纹份,__arm64__
架構(gòu)之后苟跪,isa
指針不單單只存儲了類對象和元類對象的內(nèi)存地址廷痘,而是使用共用體的方式存儲了更多信息,其中shiftcls
存儲了類對象和元類對象的內(nèi)存地址件已,需要同ISA_MASK
進(jìn)行按位與 &
運(yùn)算才可以取出其內(nèi)存地址值笋额。
更多技術(shù)知識請關(guān)注公眾號
iOS進(jìn)階