IOS底層原理之Runimte 運行時&方法的本質

前言

《cache底層分析》一文中詳細得剖析了cache的底層原理以及其相關的流程基公。那么我們有沒有留意到cahche調用insert方法之前做了哪些操作呢?哪些操作又是以什么形式傳遞的呢妒茬?
那么查看objc-cache.mm文件的頭部注釋中寫著insert()的插入時機是通過最上層的objc_msgSend觸發(fā)的仰美,如下圖:

objc-cache.mm頭部注釋

準備資料

runtime

runtime定義

編譯時

  • 編譯時 顧名思義就是正在編譯的時候. 編譯器把源代碼翻譯成機器能識別的代碼(當然只是?般意義上這么說,實際上可能只是翻譯成某個中間狀態(tài)的語?)迷殿。編譯時通過語法分析、詞法分析等編譯時類型檢查(靜態(tài)類型檢查)來發(fā)現(xiàn)代碼中的errorswarning等編譯時的錯誤信息咖杂。
  • 靜態(tài)檢查不會把代碼放內存中運?起來,?只是當作?本來掃描檢查庆寺,?些?說編譯時還分配內存啥的肯定是錯誤的說法。

運行時

  • 運?時就是代碼通過dyld被裝載到內存中執(zhí)行的過程诉字。運?時類型檢查就與前?講的編譯時類型檢查(或者靜態(tài)類型檢查)不?樣懦尝。不是簡單的掃描代碼,?是在內存中做操作和判斷壤圃。

runtime的版本

runtime有兩個版本陵霉,一個Legacy版本(早期版本),一個Modern版本(現(xiàn)行版本)伍绳。

  • 早期版本對應的編程接口:Objective-C 1.0
  • 現(xiàn)行版本對應的編程接口:Objective-C 2.0踊挠,源碼中經常看到的OBJC2
  • 早期版本用于Objective-C 1.0冲杀,32位的Mac OS X的平臺
  • 現(xiàn)行版本用于Objective-C 2.0止毕,iPhone程序和Mac OS X v10.5及以后的系統(tǒng)中的64位程序

注意:runtime就是c/c++/匯編寫的一套API

runtime三種實現(xiàn)方式

  • Objective-C方式漠趁,[penson sayHappy]
    -Framework & Serivce方式扁凛,isKindOfClass
  • Runtime API方式,class_getInstanceSize
    runtime的實現(xiàn)方式

方法的本質

探究底層又兩個方式闯传,第一種就是看匯編代碼谨朝,其次就是C/C++編譯之后的代碼。如果分析匯編代碼的話會設計到寄存器數(shù)據(jù)的一系列讀取操作甥绿,過程比較繁瑣字币,那么我們就采用第二種方式來看看方法底層的實現(xiàn)是怎么樣子的。首先編譯生成main.cpp文件共缕,然后自定義XXPerson類洗出,在XXPerson類中添加實例方法,在main函數(shù)中調用如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
 XXPerson *person = [[XXPerson alloc]init];
        [person saySomething];
        [person sayHappy:@"happy!"];
 }
    return 0;
}

xrun導出main.cpp文件图谷,查看到main函數(shù)的底層實現(xiàn)如下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        XXPerson *person = ((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
        ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)person, sel_registerName("sayHappy:"), (NSString *)&__NSConstantStringImpl__var_folders_mq_n7r4vx491nz9b2b3wpmz1mg00000gn_T_main_12c37c_mi_1);
    }
    return 0;
}

分析:

  • 通過底層的代碼翩活,方法的實現(xiàn)是通過objc_msgSend來發(fā)送的阱洪。
  • 方法的本質其實就是消息的發(fā)送

通過底層objc_msgSend來實現(xiàn)法法菠镇,情況如下:

objc_msgSend調用方法
  • objc_msgSend能夠調用類的方法冗荸,跟對象調用的結果一樣。

注意:

  • 運行項目之前必須導入<objc/message.h>頭文件利耍。
  • 關閉objc_msgSend檢查機制:target --> Build Setting -->搜索objc_msgSend -- Enable strict checking of obc_msgSend calls設置為NO蚌本。

調用類方法

創(chuàng)建XXPerson類方法sayNB,通過調用隘梨,已經底層的main.cpp可以得出一下代碼:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
 XXPerson *person = [[XXPerson alloc]init];
        [person saySomething];
        [XXPerson sayNB];
 }
    return 0;
}
//底層代碼
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        XXPerson *person = ((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("sayNB"));
    }
    return 0;
}
  • 類方法的調用也是通過objc_msgSend來進行消息發(fā)送程癌。

注意:

  • 通過之前類結構的學習,底層是不分類方法跟實例方法的轴猎,只是查找方法的地方不一樣(實例方法保存在本類鐘嵌莉,類方法保存在元類中)
  • 類方法其實就是元類的實例方法税稼。

調用父類方法

穿件XXTeacher類繼承XXPerson類,并用XXTeacher實例調用父類的saySomething方法如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
 XXTeacher *teacher = [[XXTeacher alloc]init];
        [teacher saySomething];
 }
    return 0;
}

xrun導出main.cpp文件垮斯,查看底層代碼實現(xiàn)

main底層實現(xiàn)

在用XXTeacher.m文件重寫saySomething方法郎仆,然后用xrunXXTeacher.m生成XXTeacher.cpp文件,查詢XXTeacher函數(shù)的實現(xiàn):
重寫saySomething

objc_msgSendSuper調用

  • 子類調用父類的方法可以通過objc_msgSendSuper來進行消息的發(fā)送兜蠕,其本質也就是消息的發(fā)送扰肌。
  • objc_msgSendSuper是通過向父類發(fā)送消息,與objc_msgSend流程有點不一樣熊杨。

objc_msgSendSuper的數(shù)據(jù)結構

通過查找objc4的源碼發(fā)現(xiàn):

//objc_msgSendSuper的定義
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

查看參數(shù)objc_super的結構如下(提取__OBJC2__的部分):

struct objc_super {
   //消息的接收者
    __unsafe_unretained _Nonnull id receiver;     
 //方法最先查找的class是super_class ,如果super_class查找不到會查找super_class的父類
    __unsafe_unretained _Nonnull Class super_class;  

};

objc_msgSendSuper代碼案例

通過objc_msgSendSuper的方式曙旭,調用XXPersonsaySomething方法:

objc_msgSendSuper案例分析

  • objc_msgSendSuper能夠向父類發(fā)送消息,調用父類的方法晶府。
  • 方法調用桂躏,首先在本類中找,如果沒有就到父類中找川陆。(receiver只是指定調用的是誰剂习,但是方法是在super_class找)

objc_msgSend匯編探究

首相我們在saySomething方法調用時候下個匯編斷點,如下圖:


saySomething匯編實現(xiàn)

然后我們進入objc_msgSend的匯編實現(xiàn)(打objc_msgSend的符號斷點)较沪,如下圖:


objc_msgSend匯編實現(xiàn)
  • 匯編顯示objc_msgSendlibobjc.A.dylib系統(tǒng)庫鳞绕。
  • objc_msgSend也可以在objc4的源碼中找到。

objc_msgSend在objc4源碼中的實現(xiàn)

到這里源碼的實現(xiàn)尸曼,有些同學就會想到objc_msgSend可能是c或者是c++來實現(xiàn)的们何。可是實踐告訴我們控轿,objc_msgSend的底層實現(xiàn)在源碼中是匯編語言冤竹。
源碼查找流程:在objc源碼中全局搜索objc_msgSend拂封,找到真機的匯編objc-msg-arm64.s

查找圖

源碼中寄存器的對應發(fā)生了一丟丟改變(如p0 = x0),為了方便理解方法體代碼,如下圖:
寄存器的轉換

objc_msgSend入口匯編代碼
objc_msgSend匯編實現(xiàn)

判斷receiver是否等于nil贴见, 再判斷是否支持Taggedpointer小對象類型烘苹。

  • 支持Taggedpointer小對象類型,小對象為空 片部,返回nil镣衡,不為nil處理isa獲取class跳轉CacheLookup流程 。
  • 不支持Taggedpointer小對象類型且receiver = nil档悠,跳轉LReturnZero流程返回nil廊鸥。
  • 不支持Taggedpointer小對象類型且receiver != nil,通過GetClassFromIsa_p16把獲取到class存放在p16的寄存器中辖所,然后走CacheLookup流程惰说。

GetClassFromIsa_p16獲取Class匯編流程

GetClassFromIsa_p16匯編實現(xiàn)

GetClassFromIsa_p16核心功能獲取class存放在p16寄存器。(那么就是著重看ExtractISA方法的實現(xiàn))

ExtractISA方法的匯編實現(xiàn)

// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
   ...
#else
   ...
.macro ExtractISA             //ExtractISA 主要功能 isa & ISA_MASK = class 存放到p16寄存器
    and    $0, $1, #ISA_MASK  // and 表示 & 操作缘回, $0 = $1(isa) & ISA_MASK  = class
.endmacro
// not JOP
#endif

ExtractISA主要功能isa & ISA_MASK = class 存放到p16寄存器吆视。

重點:CacheLookup匯編實現(xiàn)流程

《cache底層分析》一文中已經根據(jù)objc4底層源碼分析過整個insert的流程了,那么通過CacheLookup匯編的形式來看看這個流程跟之前的是否能夠銜接上酥宴,拭目以待@舶伞!

buckets和下標index

查找buckets與index

源碼分析:

  • 獲取_bucketsAndMaybeMask地址也就是cache的地址:p16 = isa(class)拙寡,p16 + 0x10 = _bucketsAndMaybeMask = p11授滓。
  • 獲取buckets容器的首地址:buckets = _bucketsAndMaybeMask & 0xffffffffffff(maskShift不同架構也會不同)
  • 獲取hash下標:p12 =(cmd ^ ( _cmd >> 7))& msak這一步的作用就是獲取hash下標index肆糕。

流程:isa --> _bucketsAndMaybeMask -->buckets -->hash -->index般堆。

遍歷緩存

遍歷緩存

源碼分析:

  • 根據(jù)下標index 找到index對應的bucketp13 = buckets + ((_cmd ^ (_cmd >> 7)) & mask) << (1+PTRSHIFT))诚啃。
  • 先獲取對應的bucket然后取出impsel存放到p17p9淮摔,然后*bucket--向前移動。
  • 1流程:p9= sel和 傳入的參數(shù)_cmd進行比較始赎。如果相等走2流程噩咪,如果不相等走3流程。
  • 2流程:緩存命中直接跳轉CacheHit流程极阅。
  • 3流程:判斷sel = 0條件是否成立胃碾。如果成立說明buckets里面沒有傳入的參數(shù)_cmd的緩存,沒必要往下走直接跳轉__objc_msgSend_uncached流程筋搏。如果sel 仆百!= 0說明這個bucket被別的方法占用了。你去找下一個位置看看是不是你需要的奔脐。然后在判斷下個位置的bucket和第一個bucket地址大小俄周,如果大于第一個bucket的地址跳轉1流程循環(huán)查找吁讨,如果小于等于則接繼續(xù)后面的流程。
  • 如果循環(huán)到第1bucket里都沒有找到符合的_cmd峦朗。那么會接著往下走建丧,因為下標index后面的可能還有bucket還沒有查詢。

CacheHit

// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
   ...
#else   //這是我們需要研究的
.macro TailCallCachedImp
    // $0 = cached imp, $1 = buckets, $2 = SEL, $3 = class(也就是isa)
    eor $0, $0, $3   // $0 = imp ^ class 這一步是對imp就行解碼波势,獲取運行時的imp地址
    br  $0           //調用 imp翎朱,意思是找到方法了并調用了
.endmacro
...
#endif

緩存查詢到以后直接對bucketimp進行解碼操作。即imp = imp ^ class尺铣,然后調用解碼后的imp拴曲。

遍歷緩存流程圖

帶著疑問:為什么sel = 0 的時候就直接跳出了緩存的查找呢?

遍歷緩存流程圖

分析得出:

  • 如果既沒有hash沖突又沒有目標方法的緩存凛忿,那么hash下標對應的bucket就是空的直接跳出緩存查找澈灼。
  • 不會出現(xiàn)中間是有空的bucket,兩邊有目標bucket這種情況店溢。

mask向前遍歷緩存

向前遍歷緩存

分析:

  • 找到最后一個bucket的位置:p13 = buckets + (mask << 1+3)找到最后一個bucket的位置叁熔。
  • 先獲取對應的bucket然后取出impsel存放到p17p9,然后*bucket--向前移動床牧。
  • p9= sel和 傳入的參數(shù)_cmd進行比較荣回。如果相等走2流程。
  • 如果不相等在判斷(sel 叠赦!= 0 && bucket > 第一次確定的hash下標bucket)接著循環(huán)緩存查找驹马,如果整個流程循環(huán)完仍然沒有查詢到或者遇到空的bucket革砸。說明該緩存中沒有緩存)sel = _cmd的方法除秀,緩存查詢結束跳轉__objc_msgSend_uncached流程。
  • mask向前遍歷和前面的循環(huán)遍歷邏輯基本一樣算利。

緩存查詢流程圖

緩存查詢流程

objc_msgSend流程圖

objc_msgSend流程
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末册踩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子效拭,更是在濱河造成了極大的恐慌暂吉,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缎患,死亡現(xiàn)場離奇詭異慕的,居然都是意外死亡,警方通過查閱死者的電腦和手機挤渔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門肮街,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人判导,你說我怎么就攤上這事嫉父∨婀瑁” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵绕辖,是天一觀的道長摇肌。 經常有香客問我,道長仪际,這世上最難降的妖魔是什么围小? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮弟头,結果婚禮上吩抓,老公的妹妹穿的比我還像新娘。我一直安慰自己赴恨,他們只是感情好疹娶,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著伦连,像睡著了一般雨饺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上惑淳,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天额港,我揣著相機與錄音,去河邊找鬼歧焦。 笑死移斩,一個胖子當著我的面吹牛,可吹牛的內容都是我干的绢馍。 我是一名探鬼主播向瓷,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼舰涌!你這毒婦竟也來了猖任?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤瓷耙,失蹤者是張志新(化名)和其女友劉穎朱躺,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搁痛,經...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡长搀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了鸡典。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片源请。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出巢钓,到底是詐尸還是另有隱情病苗,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布症汹,位于F島的核電站硫朦,受9級特大地震影響,放射性物質發(fā)生泄漏背镇。R本人自食惡果不足惜咬展,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瞒斩。 院中可真熱鬧破婆,春花似錦、人聲如沸胸囱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烹笔。三九已至裳扯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谤职,已是汗流浹背饰豺。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留允蜈,地道東北人冤吨。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像饶套,于是被迫代替她去往敵國和親漩蟆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內容