前言
OC是一種動(dòng)態(tài)語(yǔ)言璃赡,其動(dòng)態(tài)性是由Runtime API
來(lái)支撐的,Runtime API提供的接口都是C語(yǔ)言的 遥倦,源碼由C谤绳、C++、匯編語(yǔ)言
編寫(xiě)袒哥,想深入學(xué)習(xí)Runtime缩筛,需要先了解它底層的一些數(shù)據(jù)結(jié)構(gòu),例如isa指針
一堡称、isa指針
- 每一個(gè)繼承自NSObject的對(duì)象都有一個(gè)
isa指針
瞎抛,通過(guò)isa指針
我們可以拿到類(lèi)/元類(lèi)的內(nèi)存地址
- 每一個(gè)繼承自NSObject的對(duì)象都有一個(gè)
- 在
arm64架構(gòu)
之前,isa
就是一個(gè)普通的指針却紧,直接指向類(lèi)對(duì)象或者元類(lèi)對(duì)象桐臊,isa
直接存儲(chǔ)著類(lèi)對(duì)象、元類(lèi)對(duì)象的內(nèi)存地址
- 在
- 從
arm64架構(gòu)
開(kāi)始晓殊,對(duì)isa指針
做了優(yōu)化断凶,變成了一個(gè)union共用體
,使用了位域來(lái)存儲(chǔ)更多的信息挺物,isa指針
內(nèi)部結(jié)構(gòu)如下所示懒浮,類(lèi)對(duì)象/元類(lèi)對(duì)象的地址存儲(chǔ)在shiftcls位
,shiftcls位
占了33位识藤,由于是共用體砚著,所以需要對(duì)isa進(jìn)行一次&ISA_MASK的位運(yùn)算
,才能將類(lèi)對(duì)象的地址取出來(lái) (為何進(jìn)行一次按位與&
的運(yùn)算就能取出來(lái)shiftcls位
呢???痴昧,別著急稽穆,下面會(huì)有講到)
優(yōu)化后的isa.png
- 從
- 所謂共用體,就是指多個(gè)成員共用同一段內(nèi)存赶撰,跟結(jié)構(gòu)體對(duì)比一下舌镶,就容易理解了:結(jié)構(gòu)體的各個(gè)成員會(huì)占用不同的內(nèi)存柱彻,互相之間沒(méi)有影響;而共用體的所有成員占用同一段內(nèi)存餐胀,修改一個(gè)成員會(huì)影響其余所有成員
- 為何要進(jìn)行一次
&ISA_MASK的位運(yùn)算
才能將類(lèi)對(duì)象的內(nèi)存地址拿出來(lái)呢哟楷?看看下面的計(jì)算過(guò)程就明白了,按位與&
的規(guī)則是:相同位的兩個(gè)數(shù)字都為1否灾,則為1卖擅;若有一個(gè)不為1,則為0
- 為何要進(jìn)行一次
想把中間四位取出來(lái)墨技,應(yīng)該怎么取呢惩阶?
1010 0101
& 0011 1100
----------------------
0010 0100
只需要進(jìn)行一次 &00111100 位運(yùn)算,就可以將中間四位取出來(lái)了
這個(gè)方法用與取isa的shiftcls位的原理是一樣的扣汪,只需要 isa & ISA_MASK就可以將shiftcls位的33位給取出來(lái)了
-
isa指針
占8個(gè)字節(jié)断楷,一共有64位,每一位都有其特殊含義崭别,如下圖所示:
image.png
-
二冬筒、Class的結(jié)構(gòu)
- 我們知道
isa指針
是指向類(lèi)或者元類(lèi)的,而類(lèi)和元類(lèi)的底層數(shù)據(jù)結(jié)構(gòu)就是objc_class
結(jié)構(gòu)體紊遵,objc_class
的內(nèi)部結(jié)構(gòu)如下所示:
objc_class結(jié)構(gòu)體
- 我們知道
-
class_rw_t
里面的methods账千、properties侥蒙、protocols
是二維數(shù)組暗膜,是可讀可寫(xiě)的,包含了類(lèi)的初始內(nèi)容鞭衩、分類(lèi)的內(nèi)容
class_rw_t結(jié)構(gòu)體
-
-
class_ro_t
里面的baseMethodList学搜、baseProtocols、ivars论衍、baseProperties
是一維數(shù)組瑞佩,是只讀的,包含了類(lèi)的初始內(nèi)容坯台,如下圖所示:
class_ro_t結(jié)構(gòu)體
-
- 上述的方法列表中炬丸,都用到了
method_t
結(jié)構(gòu)體,method_t
結(jié)構(gòu)體是對(duì)方法的封裝蜒蕾,其內(nèi)存布局如下所示稠炬,其中IMP
代表函數(shù)的具體實(shí)現(xiàn);SEL
代表方法名咪啡,一般叫做選擇器首启,底層結(jié)構(gòu)跟char *類(lèi)似,不同類(lèi)中相同名字的方法撤摸,所對(duì)應(yīng)的方法選擇器是相同的毅桃,可以通過(guò)@selector和sel_registerName()
獲得褒纲;types
包含了函數(shù)返回值、參數(shù)編碼的字符串钥飞,iOS提供了一個(gè)叫做@encode
的指令莺掠,可以將具體類(lèi)型表示成字符串編碼
method_t結(jié)構(gòu)體.png
OC類(lèi)型編碼.png
- 上述的方法列表中炬丸,都用到了
三、方法緩存
-
Class
內(nèi)部結(jié)構(gòu)中有個(gè)方法緩存cache_t
读宙,用散列表來(lái)緩存曾經(jīng)調(diào)用過(guò)的方法汁蝶,提高了方法的查找速度 ,cache_t
的內(nèi)部結(jié)構(gòu)如下所示论悴,其中掖棉,_buckets
是bucket_t
結(jié)構(gòu)體的數(shù)組,bucket_t
是用來(lái)存放方法的SEL
內(nèi)存地址和IMP
膀估;_mask
的大小是數(shù)組大小 - 1幔亥;_occupied
是當(dāng)前已緩存的方法數(shù),即數(shù)組中已使用了多少位置
cache_t結(jié)構(gòu)體.png
-
- 散列表察纯,也叫哈希表帕棉,利用了數(shù)組支持下標(biāo)隨機(jī)訪(fǎng)問(wèn)的特性,通過(guò)散列函數(shù)把元素的鍵值key映射為數(shù)組的下標(biāo)饼记,然后把數(shù)據(jù)存儲(chǔ)在下標(biāo)對(duì)應(yīng)的位置香伴。按照鍵值key查找數(shù)據(jù)時(shí),只需要用同樣的散列函數(shù)具则,就可以把key轉(zhuǎn)化為數(shù)組下標(biāo)即纲,進(jìn)而從數(shù)組下標(biāo)的位置取到數(shù)據(jù),時(shí)間復(fù)雜度為O(1)博肋,如下圖所示:
-
方法緩存cache_t
就是用散列表來(lái)緩存曾經(jīng)調(diào)用過(guò)的方法的低斋,使用的散列函數(shù)是@selector(方法名) & _mask
的位運(yùn)算,其中@selector(方法名)
是方法選擇器匪凡,_mask
是散列表長(zhǎng)度 - 1膊畴,將兩者進(jìn)行一次按位與的位運(yùn)算,是為了快速算出來(lái)下標(biāo)的同時(shí)病游,保證下標(biāo)不越界
-
-
- 方法緩存到 散列表 的整個(gè)存儲(chǔ)流程是這樣的:
-
(1). 當(dāng)某個(gè)方法被調(diào)用時(shí)唇跨,就會(huì)先看方法緩存
cache_t
的buckets
中有沒(méi)有此方法:緩存中有此方法的話(huà),就直接取出來(lái)地址然后調(diào)用衬衬,不再走方法查找流程买猖;
緩存中沒(méi)有的話(huà),就會(huì)走方法查找流程佣耐,找到方法的
IMP
政勃,調(diào)用此方法的同時(shí),將方法地址緩存下來(lái)兼砖;
-
(2). 方法緩存的時(shí)候奸远,會(huì)先看
cache_t
中的buckets
有沒(méi)有初始化:如果
cache_t
中的buckets
已經(jīng)初始化了既棺,就會(huì)通過(guò)@selector(方法名) & _mask
的位運(yùn)算,計(jì)算出數(shù)組下標(biāo)懒叛,然后將Key和IMP
包裝成bucket_t
結(jié)構(gòu)體丸冕,插入到buckets
數(shù)組的對(duì)應(yīng)的下標(biāo)的位置;如果
cache_t
中的buckets
沒(méi)有初始化薛窥,就會(huì)給cache_t
中的buckets
分配大小為4的數(shù)組胖烛,并設(shè)置_mask
為3,然后通過(guò)@selector(方法名) & _mask
的位運(yùn)算诅迷,計(jì)算出數(shù)組下標(biāo)再插入
-
(3). 插入到
buckets
數(shù)組的對(duì)應(yīng)的下標(biāo)的位置的時(shí)候佩番,會(huì)看此位置有沒(méi)有被占用:如果下標(biāo)對(duì)應(yīng)的數(shù)組位置是空的,就直接將包裝好的
bucket_t
結(jié)構(gòu)體插入進(jìn)去如果下標(biāo)對(duì)應(yīng)的數(shù)組位置有值了罢杉,就將
數(shù)組下標(biāo) - 1
趟畏,看看這個(gè)新位置是不是空的,如果是空的就插入進(jìn)去滩租;如果不是空的赋秀,就繼續(xù)將數(shù)組下標(biāo) - 1
,然后比較插入律想,直到數(shù)組下標(biāo) < 0
猎莲,這個(gè)時(shí)候就將數(shù)組下標(biāo)設(shè)置為_(kāi)mask
,繼續(xù)整個(gè)插入過(guò)程 (_mask上面說(shuō)了是數(shù)組的長(zhǎng)度 - 1技即,所以不會(huì)有越界的風(fēng)險(xiǎn))
(4). 如果
buckets
數(shù)組滿(mǎn)了著洼,就會(huì)進(jìn)行擴(kuò)容,擴(kuò)容為原來(lái)大小的2倍 姥份,并且會(huì)將原來(lái)緩存的方法清空
-
- 在方法緩存的 散列表中 查找某個(gè)方法的流程是這樣的:
(1). 調(diào)用某個(gè)對(duì)象的方法時(shí)郭脂,會(huì)向這個(gè)對(duì)象發(fā)送一個(gè)
SEL
消息年碘,假設(shè)這個(gè)方法是:@selector(test)
(2). Runtime會(huì)去
objc_class結(jié)構(gòu)體
的cache方法緩存
中找澈歉,會(huì)拿@selector(test)
作為Key進(jìn)行一次散列函數(shù)計(jì)算,散列函數(shù)是@selector(方法名) & _mask
的位運(yùn)算屿衅,經(jīng)過(guò)散列函數(shù)計(jì)算出數(shù)組的下標(biāo)埃难,假設(shè)此時(shí)算出來(lái)的下標(biāo) == 2,如下圖所示-
(3).就會(huì)
buckets數(shù)組
的下標(biāo)為2的位置取出來(lái)Key涤久,與@selector(test)
進(jìn)行比較:如果Key相同涡尘,說(shuō)明找對(duì)了,就會(huì)拿這個(gè)Key的IMP去調(diào)用响迂;
如果Key不相同考抄,就將
下標(biāo) - 1
,繼續(xù)尋找相同的Key蔗彤,直到數(shù)組下標(biāo) < 0
川梅,這個(gè)時(shí)候就將數(shù)組下標(biāo)設(shè)置為_mask
疯兼,繼續(xù)整個(gè)查找過(guò)程 (_mask上面說(shuō)了是數(shù)組的長(zhǎng)度 - 1,所以不會(huì)有越界的風(fēng)險(xiǎn))
- 方法緩存的散列表贫途,是通過(guò)開(kāi)放尋址法來(lái)解決散列沖突的吧彪,所謂散列沖突,就是key不同的時(shí)候丢早,散列值hash(key)卻意外的相同了姨裸,方法緩存的散列沖突就是指,兩個(gè)不同的Key怨酝,經(jīng)過(guò)
散列函數(shù)hash(key):@selector(方法名) & _mask
算出來(lái)了同一個(gè)數(shù)組下標(biāo)傀缩,這時(shí)候就出現(xiàn)了散列沖突,就將數(shù)組下標(biāo) - 1
农猬,依次往后查找扑毡。利用散列表緩存方法,雖然會(huì)浪費(fèi)一些存儲(chǔ)空間盛险,但是卻大大提升了方法查找速度瞄摊,這也是空間換時(shí)間設(shè)計(jì)思想的具體應(yīng)用
- 方法緩存的散列表贫途,是通過(guò)開(kāi)放尋址法來(lái)解決散列沖突的吧彪,所謂散列沖突,就是key不同的時(shí)候丢早,散列值hash(key)卻意外的相同了姨裸,方法緩存的散列沖突就是指,兩個(gè)不同的Key怨酝,經(jīng)過(guò)
四、OC消息發(fā)送
OC中的方法調(diào)用苦掘,其實(shí)底層轉(zhuǎn)換成了C語(yǔ)言objc_msgSend
函數(shù)的調(diào)用换帜,objc_msgSend
的執(zhí)行分為三大階段:消息發(fā)送、動(dòng)態(tài)方法解析鹤啡、消息轉(zhuǎn)發(fā)
- 消息發(fā)送
- 動(dòng)態(tài)方法解析
- 消息轉(zhuǎn)發(fā)
五、面試題
-
- 講一下OC的消息機(jī)制
答 :OC的方法調(diào)用其實(shí)都轉(zhuǎn)成了objc_msgSend函數(shù)的調(diào)用递瑰,給
receiver方法調(diào)用者
發(fā)送了一條@selector(方法名)
消息祟牲,objc_msgSend函數(shù)底層有三大階段:消息發(fā)送、動(dòng)態(tài)方法解析抖部、消息轉(zhuǎn)發(fā) -
- OC的消息轉(zhuǎn)發(fā)流程是怎么樣的说贝?
先用調(diào)用
forwardingTargetForSelecotor:
獲取另一個(gè)消息接受者,如果獲取到了就給這個(gè)新的消息接受者慎颗,發(fā)送消息乡恕;如果獲取不到新的消息接受者,就進(jìn)入調(diào)用
methodSignatureForSelector:
獲取方法簽名俯萎,如果獲取到了方法簽名傲宜,就調(diào)用forwardInvocation:
方法,在這個(gè)方法中可以自定義任何邏輯如果拿不到方法簽名夫啊,就調(diào)用
doesNotRecognizaSelector:
方法函卒,拋出異常
-
- RunTime有哪些具體應(yīng)用?
利用關(guān)聯(lián)對(duì)象給分類(lèi)增加屬性
遍歷類(lèi)的所有成員變量撇眯,實(shí)現(xiàn)字典轉(zhuǎn)模型报嵌、自動(dòng)歸檔解檔
交換方法實(shí)現(xiàn)
利用消息轉(zhuǎn)發(fā)機(jī)制躁愿,避免方法找不到而產(chǎn)生崩潰