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];
中的
問題2
運行結果
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對象的本質(上) —— 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系列文章
Runtime原理探究(一)—— isa的深入體會(蘋果對isa的優化)
Runtime原理探究(二)—— Class結構的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機制
Runtime原理探究(五)—— super的本質
[Runtime原理探究(六)—— Runtime的應用...待續]-()
[Runtime原理探究(七)—— Runtime的API...待續]-()
Runtime原理探究(八)—— 面試題中的Runtime