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


Runtime系列文章

Runtime原理探究(一)—— isa的深入體會(huì)(蘋果對(duì)isa的優(yōu)化)
Runtime原理探究(二)—— Class結(jié)構(gòu)的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問(wèn)底消息機(jī)制
Runtime原理探究(五)—— super的本質(zhì)
[Runtime原理探究(六)—— Runtime的應(yīng)用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
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

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

運(yùn)行結(jié)果

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

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

現(xiàn)在,我們就靜下心來(lái),好好來(lái)搞一搞。

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

我們先回顧一下正常人是怎么調(diào)用方法的

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

相信對(duì)于上面的代碼沒(méi)有人會(huì)有疑問(wèn),我們通過(guò)一張圖來(lái)說(shuō)明一下,這兩行代碼運(yùn)行時(shí),內(nèi)存里面的情況


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

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

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

請(qǐng)看圖中的文字說(shuō)明,因?yàn)閺谋举|(zhì)上說(shuō),
指針person-->指針isa-->[CLPerson class]
指針obj-->指針cls-->[CLPerson class]
因此[person print]效果 == [(__bridge id)obj print]效果,這里需要仔細(xì)體會(huì)一下。

回想一下消息發(fā)送的本質(zhì),[person print]是從person所指向的結(jié)構(gòu)體(實(shí)例對(duì)象)取出第一個(gè)成員變量isa,然后根據(jù)isa找到對(duì)應(yīng)Class對(duì)象的內(nèi)存空間,最后在Class對(duì)象的方法列表里面進(jìn)行方法查找,最后調(diào)用方法。

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

②打印結(jié)果為什么是<ViewController: 0x7fce43e08aa0>

這個(gè)問(wèn)題有點(diǎn)小復(fù)雜,不過(guò)沒(méi)關(guān)系,我們一步一步來(lái)

print方法找到后的調(diào)用過(guò)程
我們知道任何OC方法的底層都是一個(gè)C函數(shù),并且函數(shù)頭兩個(gè)參數(shù)是默認(rèn)參數(shù)id selfSEL _cmd,那么self是誰(shuí)呢?以上面代碼為例

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

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

print方法對(duì)應(yīng)的C函數(shù)里面,self就是person,而print的內(nèi)容是打印self.name,也就是必然要通過(guò)self,找到成員變量_name,如何找呢,這就需要我們來(lái)了解一下實(shí)例對(duì)象的內(nèi)存布局,根據(jù)我們上面有關(guān)CLPerson類的定義,實(shí)例變量person的內(nèi)存布局如下圖

self.name相當(dāng)于self->_name,因?yàn)?code>_name是isa后面緊接著的成員變量,而_name是一個(gè)指針,占8個(gè)字節(jié)大小,因此self->_name實(shí)際上得到的就是從self所指向的內(nèi)存地址往高地址偏移8個(gè)字節(jié)(跨過(guò)isa的大?。┖蟮膬?nèi)存地址,指向一段8字節(jié)大小的內(nèi)存空間,從而獲得person對(duì)象的成員變量_name。

如果你還不太了解OC對(duì)象內(nèi)存布局相關(guān)知識(shí)的,可以參考
OC對(duì)象的本質(zhì)(上) —— OC對(duì)象的底層實(shí)現(xiàn)原理
OC對(duì)象的本質(zhì)(下)—— 詳解isa&superclass指針

我在其中進(jìn)行了詳細(xì)闡述。 如果對(duì)于上面的內(nèi)容沒(méi)有疑問(wèn),那么下面接著看面試題中設(shè)置的場(chǎng)景,在分析print方法為何能被調(diào)用的過(guò)程中,我們可以看到實(shí)際上

  • obj指針相當(dāng)于person指針(也就是print方法里面的self
  • cls指針相當(dāng)于person指針所指向的實(shí)例對(duì)象里面的isa指針
    所以對(duì)于面試題的場(chǎng)景,實(shí)際上是這樣的

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

因?yàn)?code>obj,cls都是viewDidLoad方法(函數(shù))里面的局部變量,我們知道函數(shù)的局部變量都是放在??臻g里面的。那么你了解函數(shù)的??臻g嗎?我們來(lái)簡(jiǎn)單科普一下。

函數(shù)的棧空間簡(jiǎn)介

棧空間的作用,是用來(lái)存放被調(diào)用函數(shù)其內(nèi)部所定義的局部變量的。對(duì)于arm64架構(gòu)來(lái)說(shuō),這么理解就夠了,如果你恰好了解過(guò)8086匯編,那么可能知道,??臻g里面還會(huì)存放函數(shù)的參數(shù),但是對(duì)于arm64來(lái)說(shuō),函數(shù)的參數(shù)通常會(huì)放到寄存器里面,所以我們就先簡(jiǎn)單的認(rèn)為,函數(shù)的??臻g里面放的就是函數(shù)的局部變量。而且局部變量的存放順序,是根據(jù)定義的先后順序,從函數(shù)棧底開(kāi)始,一個(gè)一個(gè)排列,最先定義的局部變量位于棧底(高地址),通過(guò)下圖來(lái)描繪一下

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

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

我們看到,viewDidLoad內(nèi)部只有兩個(gè)局部變量,分別是id clsvoid *obj,其余的都是方法調(diào)用。那么棧里面的情況應(yīng)該就是

可以看出如果按圖中的分析,print方法將會(huì)最終打印棧底之外8個(gè)字節(jié)里面的內(nèi)容,但是我們知道一個(gè)函數(shù)內(nèi)部是不能訪問(wèn)其他函數(shù)的??臻g的,上圖中的這8個(gè)字節(jié)明顯超出了當(dāng)前函數(shù)的??臻g,所以無(wú)法解釋我們上面看到的打印結(jié)果。

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

[super viewDidLoad];展開(kāi)成底層函數(shù)就是

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

注意這個(gè)函數(shù)的第一個(gè)參數(shù)是一個(gè)結(jié)構(gòu)體__rw_objc_super,那么這個(gè)結(jié)構(gòu)體參數(shù)實(shí)際上是在當(dāng)前viewDidLoad函數(shù)的作用域里面被定義賦值,然后再傳入objc_msgSendSuper作為參數(shù)的。說(shuō)白了viewDidLoad還含有一個(gè)隱藏局部變量,其內(nèi)部實(shí)際上等同于這么寫

//    [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內(nèi)部第一個(gè)局部變量實(shí)際上是一個(gè)結(jié)構(gòu)體類型struct __rw_objc_super的變量,該結(jié)構(gòu)體內(nèi)部有兩個(gè)id類型(也就是指針變量)的成員變量,并且注意,第一個(gè)成員變量是 self,而這個(gè)self正式當(dāng)前方法的消息接受者,也就是ViewController實(shí)例對(duì)象。需要說(shuō)明的是,這個(gè)self跟我們上面討論print方法里面用到的那個(gè)self是不同的兩個(gè)對(duì)象哦,請(qǐng)用心體會(huì)。好了,說(shuō)多了太繞,直接上圖

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

接下來(lái),我們通過(guò)匯編手段來(lái)驗(yàn)證一下上面推斷,我們先將程序運(yùn)行至下圖所示的斷點(diǎn)處


此時(shí), viewDidLoad函數(shù)棧上所有的局部變量已經(jīng)賦值完畢,匯編情況如下

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

(lldb) register read rbp
     rbp = 0x00007ffeeaddd130

rbp - 0x30 = 0x7FFEEADDD100 這樣就得到了棧頂?shù)牡奈恢?,然后打印出棧頂位?之后的48字節(jié)內(nèi)容(也就是當(dāng)前的函數(shù)??臻g)


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

也就是下圖所示



我們可以挨個(gè)打印一下每一個(gè)局部變量

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

(lldb) po 0x0000000104e245c8
CLPerson

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

(lldb) po 0x0000000104e24500
ViewController

(lldb) po 0x00007fff527257c0
140734576613312

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

你或許會(huì)好奇為什么_cmd所指向的內(nèi)容打出來(lái)的為什么是140734576613312(=0x00007fff527257c0,也就是它自己),根據(jù)_cmd的地址0x00007fff527257c0,說(shuō)明它也是??臻g的地址,因?yàn)?code>_cmd其實(shí)是viewDidLoad上層函數(shù)傳過(guò)來(lái)的參數(shù),因此這個(gè)??臻g應(yīng)該是外層函數(shù)的局部變量,也就是說(shuō)_cmd本質(zhì)上說(shuō)是一個(gè)指針。那我們看一下所指向的這段內(nèi)存里面放了什么內(nèi)容,因?yàn)椴恢谰唧w的大小,所以我們通過(guò)Xcode的內(nèi)存查看器來(lái)看看


原來(lái)就是函數(shù)viewDidLoad所對(duì)應(yīng)的函數(shù)名字符串而已,這樣所以的疑問(wèn)就掃清了。。。??????

這道面試題確實(shí)有點(diǎn)扯,項(xiàng)目中也絕不會(huì)這么寫代碼,但從面試的角度,這里面涉及了對(duì)于函數(shù)??臻g的理解,對(duì)于super本質(zhì)的理解,對(duì)于消息機(jī)制的理解對(duì)于OC對(duì)象本質(zhì)的理解,在高考里面,屬于最后一道大題的難度級(jí)別,本文之前,你可能祈禱千萬(wàn)別碰到這種變態(tài)的面試題,但是本文過(guò)后,如果你能完全掌握里面的精髓,我相信大家肯定會(huì)祈禱面試碰到這道題,因?yàn)楣馐前牙锩嫔婕暗降乃膫€(gè)對(duì)于...的理解都展開(kāi)講一遍,那一般的面試官估計(jì)就要被您給反虐了:)

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


Runtime系列文章

Runtime原理探究(一)—— isa的深入體會(huì)(蘋果對(duì)isa的優(yōu)化)
Runtime原理探究(二)—— Class結(jié)構(gòu)的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問(wèn)底消息機(jī)制
Runtime原理探究(五)—— super的本質(zhì)
[Runtime原理探究(六)—— Runtime的應(yīng)用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容