Runtime原理探究(一)—— isa的深刻體會(蘋果對isa的優化)程序員
Runtime原理探究(二)—— Class結構的深刻分析面試
Runtime原理探究(三)—— OC Class的方法緩存cache_t緩存
Runtime原理探究(四)—— 刨根問底消息機制markdown
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];
中的
問題2
運行結果post
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
指向CLPerson
的Class
對象,而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
指針指向的就是CLPerson
的Class對象
的內存空間,因此一樣能夠進入到它的方法列表進行查找,最後找到print
方法進行調用,到此問題①解釋完畢。
<ViewController: 0x7fce43e08aa0>
這個問題有點小複雜,不過不要緊,咱們一步一步來
print方法找到後的調用過程 咱們知道任何OC方法的底層都是一個C函數,而且函數頭兩個參數是默認參數id self
和 SEL _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
,由於_name
是isa
後面緊接着的成員變量,而_name
是一個指針,佔8
個字節大小,所以self->_name
實際上獲得的就是從self
所指向的內存地址往高地址偏移8個字節(跨過isa
的大小)後的內存地址,指向一段8字節大小的內存空間,從而得到person
對象的成員變量_name
。
若是你還不太瞭解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
對象。
由於obj
,cls
都是viewDidLoad
方法(函數)裏面的局部變量,咱們知道函數的局部變量都是放在棧空間裏面的。那麼你瞭解函數的棧空間嗎?咱們來簡單科普一下。
棧空間的做用,是用來存放被調用函數其內部所定義的局部變量的。對於arm64
架構來講,這麼理解就夠了,若是你剛好了解過8086
彙編,那麼可能知道,棧空間裏面還會存放函數的參數,可是對於arm64
來講,函數的參數一般會放到寄存器裏面,因此咱們就先簡單的認爲,函數的棧空間裏面放的就是函數的局部變量。並且局部變量的存放順序,是根據定義的前後順序,從函數棧底開始,一個一個排列,最早定義的局部變量位於棧底(高地址),經過下圖來描繪一下
那麼咱們就來看一下viewDidLoad
裏面總共有哪些局部變量,再貼一下代碼
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [CLPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}
複製代碼
咱們看到,viewDidLoad
內部只有兩個局部變量,分別是id cls
和void *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個字節,也就是當前方法的消息接受者self
(ViewController實例對象
),所以打印的結果是<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原理探究(一)—— isa的深刻體會(蘋果對isa的優化)