Runtime原理探究(八)—— Runtime綜合面試題


Runtime系列文章

Runtime原理探究(一)—— isa的深入體會(蘋果對isa的優化)
Runtime原理探究(二)—— Class結構的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機制
Runtime原理探究(五)—— super的本質
[Runtime原理探究(六)—— Runtime的應用...待續]-()
[Runtime原理探究(七)—— Runtime的API...待續]-()
Runtime原理探究(八)—— 面試題中的Runtime

先上面試題

//***********????CLPerson.h????************

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CLPerson : NSObject
@property (nonatomic, copy) NSString *name;
-(void)print;
@end

NS_ASSUME_NONNULL_END


//***********????CLPerson.m????************ 

#import "CLPerson.h"

@implementation CLPerson

-(void)print {
    NSLog(@"My name's %@", self.name);
}

@end

//***********????ViewController.m????************ 

#import "ViewController.h"
#import "CLPerson.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [CLPerson class];
    void *obj = &cls;
    [(__bridge id)obj print];  
}

@end

問題1 [(__bridge id)obj print];中的print方法可以被正常調用嗎?
問題2 print方法最終的打印結果是什么?

運行結果

2019-08-13 17:10:58.075381+0800 iOS-Runtime[29076:3163099] My name's <ViewController: 0x7fce43e08aa0>

從運行結果,print方法可以被成功調用,打印結果是My name's <ViewController: 0x7fce43e08aa0>,從代碼到運行結果,似乎莫名其妙。如果我在毫無防備的情況下碰到這樣的面試題,我會選擇選擇直接起身,優雅離去,同時心里默念WHAT THE FUCK!!!

現在,我們就靜下心來,好好來搞一搞。

[(__bridge id)obj print];中的print方法為什么可以被正常調用?

我們先回顧一下正常人是怎么調用方法的

CLPerson *person = [[CLPerson alloc] init];
[person print];

相信對于上面的代碼沒有人會有疑問,我們通過一張圖來說明一下,這兩行代碼運行時,內存里面的情況


再看看我們面試題里面的代碼

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [CLPerson class];
    void *obj = &cls;
    [(__bridge id)obj print];  
}

可以看出,cls指向CLPersonClass對象,而obj指向cls,如下圖示

請看圖中的文字說明,因為從本質上說,
指針person-->指針isa-->[CLPerson class]
指針obj-->指針cls-->[CLPerson class]
因此[person print]效果 == [(__bridge id)obj print]效果,這里需要仔細體會一下。

回想一下消息發送的本質[person print]是從person所指向的結構體(實例對象)取出第一個成員變量isa,然后根據isa找到對應Class對象的內存空間,最后在Class對象的方法列表里面進行方法查找,最后調用方法。

那么[(__bridge id)obj print],同樣會遵從上面的流程,因為obj所指向的是一個cls指針變量地址,恰巧,這個cls指針指向的就是CLPersonClass對象的內存空間,所以同樣可以進入到它的方法列表進行查找,最后找到print方法進行調用,到此問題①解釋完畢。

②打印結果為什么是<ViewController: 0x7fce43e08aa0>

這個問題有點小復雜,不過沒關系,我們一步一步來

print方法找到后的調用過程
我們知道任何OC方法的底層都是一個C函數,并且函數頭兩個參數是默認參數id selfSEL _cmd,那么self是誰呢?以上面代碼為例

CLPerson *person = [[CLPerson alloc] init];
[person print];

**********
-(void)print {
    NSLog(@"My name's %@", self.name);
}

print方法對應的C函數里面,self就是person,而print的內容是打印self.name,也就是必然要通過self,找到成員變量_name,如何找呢,這就需要我們來了解一下實例對象的內存布局,根據我們上面有關CLPerson類的定義,實例變量person的內存布局如下圖

self.name相當于self->_name,因為_nameisa后面緊接著的成員變量,而_name是一個指針,占8個字節大小,因此self->_name實際上得到的就是從self所指向的內存地址往高地址偏移8個字節(跨過isa的大小)后的內存地址,指向一段8字節大小的內存空間,從而獲得person對象的成員變量_name

如果你還不太了解OC對象內存布局相關知識的,可以參考
OC對象的本質(上) —— OC對象的底層實現原理
OC對象的本質(下)—— 詳解isa&superclass指針

我在其中進行了詳細闡述。 如果對于上面的內容沒有疑問,那么下面接著看面試題中設置的場景,在分析print方法為何能被調用的過程中,我們可以看到實際上

  • obj指針相當于person指針(也就是print方法里面的self
  • cls指針相當于person指針所指向的實例對象里面的isa指針
    所以對于面試題的場景,實際上是這樣的

兩張圖本質是一樣的,只不過在面試題的場景里,print方法被調用的時候,其內部的self = obj,因此self.name作用就是從obj所指向的內存空間,往高地址偏移8個字節,而obj指向了cls的內存地址,cls也是是一個指針,所以占8個字節,因此self.name取到的實際上恰好是指針變量cls之后接下來的一段8字節內存空間,所以最終print打印出的就是這段內存里面存儲的內容。而結果我們已經看到了,打印的是<ViewController: 0x7fce43e08aa0>,接下來我們就要分析一下為啥cls下面存著的是ViewController對象。

因為objcls都是viewDidLoad方法(函數)里面的局部變量,我們知道函數的局部變量都是放在棧空間里面的。那么你了解函數的棧空間嗎?我們來簡單科普一下。

函數的棧空間簡介

棧空間的作用,是用來存放被調用函數其內部所定義的局部變量的。對于arm64架構來說,這么理解就夠了,如果你恰好了解過8086匯編,那么可能知道,棧空間里面還會存放函數的參數,但是對于arm64來說,函數的參數通常會放到寄存器里面,所以我們就先簡單的認為,函數的棧空間里面放的就是函數的局部變量。而且局部變量的存放順序,是根據定義的先后順序,從函數棧底開始,一個一個排列,最先定義的局部變量位于棧底(高地址),通過下圖來描繪一下

那么我們就來看一下viewDidLoad里面總共有哪些局部變量,再貼一下代碼

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [CLPerson class];
    void *obj = &cls;
    [(__bridge id)obj print];  
}

我們看到,viewDidLoad內部只有兩個局部變量,分別是id clsvoid *obj,其余的都是方法調用。那么棧里面的情況應該就是

可以看出如果按圖中的分析,print方法將會最終打印棧底之外8個字節里面的內容,但是我們知道一個函數內部是不能訪問其他函數的棧空間的,上圖中的這8個字節明顯超出了當前函數的棧空間,所以無法解釋我們上面看到的打印結果。

其實,這個面試題里面設計了一個很隱藏的貓膩。問題的出口其實是在[super viewDidLoad];這句代碼上,關于super問題,可以參考我在Runtime筆記(五)—— super的本質一文中的解析。這里就直接基于文章中的知識來解決我們當前的問題了。

[super viewDidLoad];展開成底層函數就是

objc_msgSendSuper((__rw_objc_super){
            (id)self,   
            (id)class_getSuperclass(objc_getClass("ViewController"))
           },   
            @selector(viewDidLoad));

注意這個函數的第一個參數是一個結構體__rw_objc_super,那么這個結構體參數實際上是在當前viewDidLoad函數的作用域里面被定義賦值,然后再傳入objc_msgSendSuper作為參數的。說白了viewDidLoad還含有一個隱藏局部變量,其內部實際上等同于這么寫

//    [super viewDidLoad];
    struct __rw_objc_super arg = {
        (id)self,
        (id)class_getSuperclass(objc_getClass("ViewController"))
    };
    
    objc_msgSendSuper(arg, @selector(viewDidLoad));
    
    id cls = [CLPerson class];
    void *obj = &cls;
    [(__bridge id)obj print];

所以,viewDidLoad內部第一個局部變量實際上是一個結構體類型struct __rw_objc_super的變量,該結構體內部有兩個id類型(也就是指針變量)的成員變量,并且注意,第一個成員變量是 self,而這個self正式當前方法的消息接受者,也就是ViewController實例對象。需要說明的是,這個self跟我們上面討論print方法里面用到的那個self是不同的兩個對象哦,請用心體會。好了,說多了太繞,直接上圖

綜上所述,print里面通過self.name所拿到的變量,就是圖中cls下面的那8個字節,也就是當前方法的消息接受者selfViewController實例對象),因此打印的結果是<ViewController: 0x7fce43e08aa0>,好了,所有的問題就都得到解釋了。

接下來,我們通過匯編手段來驗證一下上面推斷,我們先將程序運行至下圖所示的斷點處


此時, viewDidLoad函數棧上所有的局部變量已經賦值完畢,匯編情況如下

從上面的分析可以看出,viewDidLoad函數棧空間大小為48個字節,存放了6個局部變量,每個局部變量8個字節,棧空間的地址范圍是[rbp-0x30] ~ [rbp],因此想要查看當前棧空間里面內容,可以利用如下LLDB指令:
先讀出當前棧底位置,也就寄存器rbp的值

(lldb) register read rbp
     rbp = 0x00007ffeeaddd130

rbp - 0x30 = 0x7FFEEADDD100 這樣就得到了棧頂的的位置,然后打印出棧頂位置 之后的48字節內容(也就是當前的函數棧空間)


(lldb) x/6xg 0x7FFEEADDD100
0x7ffeeaddd100: 0x00007ffeeaddd108 0x0000000104e245c8
0x7ffeeaddd110: 0x00007f9d01508f50 0x0000000104e24500
0x7ffeeaddd120: 0x00007fff527257c0 0x00007f9d01508f50

也就是下圖所示



我們可以挨個打印一下每一個局部變量

(lldb) po 0x00007ffeeaddd108
<CLPerson: 0x7ffeeaddd108>

(lldb) po 0x0000000104e245c8
CLPerson

(lldb) po 0x00007f9d01508f50   -->?????? 實際上 [(__bridge id)obj print]; 的本質就等同于這一句??????
<ViewController: 0x7f9d01508f50>

(lldb) po 0x0000000104e24500
ViewController

(lldb) po 0x00007fff527257c0
140734576613312

(lldb) po 0x00007f9d01508f50
<ViewController: 0x7f9d01508f50>

你或許會好奇為什么_cmd所指向的內容打出來的為什么是140734576613312(=0x00007fff527257c0,也就是它自己),根據_cmd的地址0x00007fff527257c0,說明它也是棧空間的地址,因為_cmd其實是viewDidLoad上層函數傳過來的參數,因此這個棧空間應該是外層函數的局部變量,也就是說_cmd本質上說是一個指針。那我們看一下所指向的這段內存里面放了什么內容,因為不知道具體的大小,所以我們通過Xcode的內存查看器來看看


原來就是函數viewDidLoad所對應的函數名字符串而已,這樣所以的疑問就掃清了。。。??????

這道面試題確實有點扯,項目中也絕不會這么寫代碼,但從面試的角度,這里面涉及了對于函數棧空間的理解對于super本質的理解對于消息機制的理解對于OC對象本質的理解,在高考里面,屬于最后一道大題的難度級別,本文之前,你可能祈禱千萬別碰到這種變態的面試題,但是本文過后,如果你能完全掌握里面的精髓,我相信大家肯定會祈禱面試碰到這道題,因為光是把里面涉及到的四個對于...的理解都展開講一遍,那一般的面試官估計就要被您給反虐了:)

好了,關于面試的話題,到此結束,希望對大家有幫助,文中如有解釋的不透徹或者不正確的地方,歡迎交流指正,程序員的世界沒有容易二字,加油,與諸君共勉??????。


Runtime系列文章

Runtime原理探究(一)—— isa的深入體會(蘋果對isa的優化)
Runtime原理探究(二)—— Class結構的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機制
Runtime原理探究(五)—— super的本質
[Runtime原理探究(六)—— Runtime的應用...待續]-()
[Runtime原理探究(七)—— Runtime的API...待續]-()
Runtime原理探究(八)—— 面試題中的Runtime

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372