iOS底層原理(二):RunTime底層原理

前言

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指針
    1. 每一個(gè)繼承自NSObject的對(duì)象都有一個(gè)isa指針瞎抛,通過(guò)isa指針我們可以拿到類(lèi)/元類(lèi)的內(nèi)存地址
    1. arm64架構(gòu)之前,isa就是一個(gè)普通的指針却紧,直接指向類(lèi)對(duì)象或者元類(lèi)對(duì)象桐臊,isa直接存儲(chǔ)著類(lèi)對(duì)象、元類(lèi)對(duì)象的內(nèi)存地址
    1. 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
    1. 所謂共用體,就是指多個(gè)成員共用同一段內(nèi)存赶撰,跟結(jié)構(gòu)體對(duì)比一下舌镶,就容易理解了:結(jié)構(gòu)體的各個(gè)成員會(huì)占用不同的內(nèi)存柱彻,互相之間沒(méi)有影響;而共用體的所有成員占用同一段內(nèi)存餐胀,修改一個(gè)成員會(huì)影響其余所有成員
    1. 為何要進(jìn)行一次&ISA_MASK的位運(yùn)算才能將類(lèi)對(duì)象的內(nèi)存地址拿出來(lái)呢哟楷?看看下面的計(jì)算過(guò)程就明白了,按位與&的規(guī)則是:相同位的兩個(gè)數(shù)字都為1否灾,則為1卖擅;若有一個(gè)不為1,則為0
想把中間四位取出來(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)了
    1. isa指針占8個(gè)字節(jié)断楷,一共有64位,每一位都有其特殊含義崭别,如下圖所示:
      image.png
二冬筒、Class的結(jié)構(gòu)
    1. 我們知道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)體
    1. class_rw_t里面的methods账千、properties侥蒙、protocols是二維數(shù)組暗膜,是可讀可寫(xiě)的,包含了類(lèi)的初始內(nèi)容鞭衩、分類(lèi)的內(nèi)容
      class_rw_t結(jié)構(gòu)體
    1. class_ro_t里面的baseMethodList学搜、baseProtocols、ivars论衍、baseProperties是一維數(shù)組瑞佩,是只讀的,包含了類(lèi)的初始內(nèi)容坯台,如下圖所示:
      class_ro_t結(jié)構(gòu)體
    1. 上述的方法列表中炬丸,都用到了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
三、方法緩存
    1. Class內(nèi)部結(jié)構(gòu)中有個(gè)方法緩存cache_t读宙,用散列表來(lái)緩存曾經(jīng)調(diào)用過(guò)的方法汁蝶,提高了方法的查找速度cache_t的內(nèi)部結(jié)構(gòu)如下所示论悴,其中掖棉,_bucketsbucket_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
    1. 散列表察纯,也叫哈希表帕棉,利用了數(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)博肋,如下圖所示:
將key經(jīng)過(guò)散列函數(shù)轉(zhuǎn)化為數(shù)組下標(biāo).png
    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)不越界
    1. 方法緩存到 散列表 的整個(gè)存儲(chǔ)流程是這樣的:
    • (1). 當(dāng)某個(gè)方法被調(diào)用時(shí)唇跨,就會(huì)先看方法緩存cache_tbuckets中有沒(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)緩存的方法清空

    1. 在方法緩存的 散列表中 查找某個(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))

buckets數(shù)組.png
    1. 方法緩存的散列表贫途,是通過(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)用
四、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ā)

    1. 消息發(fā)送
消息發(fā)送流程
    1. 動(dòng)態(tài)方法解析
動(dòng)態(tài)方法解析流程.png
動(dòng)態(tài)方法解析時(shí)惯驼,增加方法.png
    1. 消息轉(zhuǎn)發(fā)
消息轉(zhuǎn)發(fā)流程.png
生成NSMethodSignature的兩種方法
五、面試題
    1. 講一下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ā)

    1. OC的消息轉(zhuǎn)發(fā)流程是怎么樣的说贝?
    • 先用調(diào)用forwardingTargetForSelecotor:獲取另一個(gè)消息接受者,如果獲取到了就給這個(gè)新的消息接受者慎颗,發(fā)送消息乡恕;

    • 如果獲取不到新的消息接受者,就進(jìn)入調(diào)用methodSignatureForSelector:獲取方法簽名俯萎,如果獲取到了方法簽名傲宜,就調(diào)用forwardInvocation:方法,在這個(gè)方法中可以自定義任何邏輯

    • 如果拿不到方法簽名夫啊,就調(diào)用doesNotRecognizaSelector:方法函卒,拋出異常

    1. 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)生崩潰

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市沪蓬,隨后出現(xiàn)的幾起案子彤钟,更是在濱河造成了極大的恐慌,老刑警劉巖跷叉,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逸雹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡云挟,警方通過(guò)查閱死者的電腦和手機(jī)梆砸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)园欣,“玉大人帖世,你說(shuō)我怎么就攤上這事》锌荩” “怎么了日矫?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)绑榴。 經(jīng)常有香客問(wèn)我哪轿,道長(zhǎng),這世上最難降的妖魔是什么翔怎? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任窃诉,我火速辦了婚禮,結(jié)果婚禮上赤套,老公的妹妹穿的比我還像新娘飘痛。我一直安慰自己,他們只是感情好容握,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布宣脉。 她就那樣靜靜地躺著,像睡著了一般唯沮。 火紅的嫁衣襯著肌膚如雪脖旱。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天昔榴,我揣著相機(jī)與錄音踪古,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛搁宾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼巍虫!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起鳍刷,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤占遥,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后输瓜,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體瓦胎,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年尤揣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了搔啊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡北戏,死狀恐怖负芋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嗜愈,我是刑警寧澤旧蛾,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站蠕嫁,受9級(jí)特大地震影響蚜点,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拌阴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一绍绘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧迟赃,春花似錦陪拘、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至酌媒,卻和暖如春欠痴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背秒咨。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工喇辽, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人雨席。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓菩咨,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抽米,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355