說到iOS,要麼公司規模比較小,<=3人,不需要面試。 其他的,大概率要讓你刀槍棍棒十八般武藝都拿出來耍耍。 而其中,但凡敵軍陣營中有iOSer的,又極大概率會考到 Runtime 的知識點。 以下,是一題 sunnyxx的一道 runtime 考題,給大伙練練手,如果掌握了,Runtime層面的初 ...
說到iOS,要麼公司規模比較小,<=3人,不需要面試。
其他的,大概率要讓你刀槍棍棒十八般武藝都拿出來耍耍。
而其中,但凡敵軍陣營中有iOSer的,又極大概率會考到 Runtime 的知識點。
以下,是一題 sunnyxx的一道 runtime 考題,給大伙練練手,如果掌握了,Runtime層面的初中級問題應該都不在話下~
題目來襲:
//MNPerson @interface MNPerson : NSObject @property (nonatomic, copy)NSString *name; - (void)print; @end @implementation MNPerson - (void)print{ NSLog(@"self.name = %@",self.name); } @end --------------------------------------------------- @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; id cls = [MNPerson class]; void *obj = &cls; [(__bridge id)obj print]; }
問輸出結果是啥,會不會崩潰。
最終結果:
self.name = <ViewController: 0x7fe667608ae0>
what?
- 問題1:print 是實例方法,但是並沒有哪裡調用了
[MNPerson alloc]init]
?? - 問題2: 為啥列印了 viewController?
當前記憶體地址結構 - 與正常的[person print]
對比
- person變數的指針,執行 MNPerson 實例對象
- 實例對象的本身是個結構體,之前指向他,等價於執行結構體的第一個成員
- 結構體的第一個成員是isa,所以可以理解為,person->isa
- 所以兩個print,其實記憶體結構一致
- obj -> cls -> [MNPerson Class]
- person -> isa -> [MNPerson Class]
調用print 方法,不需要關心有沒有成員變數
_name
,所以可以理解為,cls == isa
- 函數調用,是通過查找isa,其實本質,是查找結構體的前八個位元組;
- 前八個位元組正好是isa,所以這裡可以理解為 cls == isa,這麼理解的話,cls其實等於isa;
- 所以可以找得到 MNPerson 類,就可以找到MNPerson 類內部的方法,從而調用
print
函數
問題2:為啥裡面列印的是 ViewController
這就需要瞭解到iOS的記憶體分配相關知識
記憶體分配
void test(){
int a = 4;
int b = 5;
int c = 6;
NSLog(@"a = %p,b = %p,c = %p",&a,&b,&c);
}
---------------------------
a = 0x7ffee87e9fdc,
b = 0x7ffee87e9fd8,
c = 0x7ffee87e9fd4
- 局部變數是在棧空間
- 上圖可以發現,a先定義,a的地址比b高,得出結論:棧的記憶體分配是從高地址到低地址
- 棧的記憶體是連續的 (這點也很重要!!)
OC方法的本質,其實是函數調用, 底層就是調用 objc_msgSend() 函數發送消息。
- (void)viewDidLoad { [super viewDidLoad]; NSString *test = @"666"; id cls = [MNPerson class]; void *obj = &cls; [(__bridge id)obj print]; }
以上述代碼為例,三個變數 - test、cls、obj,都是局部變數,所以都在棧空間
棧空間是從高地址到低地址分配,所以test是最高地址,而obj是最低地址
MNPerson底層結構
struct MNPerson_IMPL{ Class isa; NSString *_name; } - (void)print{ NSLog(@"self.name = %@",self->_name); }
- 要列印的
_name
成員變數,其實是通過self ->
去查找; - 這裡的 self,就是函數調用者;
[(__bridge id)obj print];
即通過 obj 開始找;- 而找
_name
,是通過指針地址查找,找得MNPerson_IMPL
結構體 - 因為這裡的
MNPerson_IMPL
裡面就兩個變數,所以這裡查找_name
,就是通過isa
的地址,跳過8個位元組,找到_name
而前面又說過,cls = isa,而_name 的地址 = isa往下偏移 8 個位元組,所以上面的圖可以轉成
_name的本質,先找到 isa,然後跳過 isa 的八個位元組,就找到 _name這個變數
所以上圖輸出
self.name = 666
最早沒有 test變數的時候呢
- (void)viewDidLoad { [super viewDidLoad]; id cls = [MNPerson class]; void *obj = &cls; [(__bridge id)obj print]; }
[super viewDidLoad];做了什麼
底層 - objc_msgSendSuper
objc_msgSendSuper({ self, [ViewController class] },@selector(ViewDidLoad)),
等價於:
struct temp = { self, [ViewController class] } objc_msgSendSuper(temp, @selector(ViewDidLoad))
所以等於有個局部變數 - 結構體 temp,
結構體的地址 = 他的第一個成員,這裡的第一個成員是self
所以等價於 _name = self = 當前ViewController,所以最後輸出
self.name = <ViewController: 0x7fc6e5f14970>
話外篇 super 的本質
**其實super的本質,不是 objc_msgSendSuper({self,[super class],@selector(xxx)}) **
而是
objc_msgSendSuper2( {self, [current class]//當前類 }, @selector(xxx)})
函數內部邏輯,拿到第二個成員 - 當前類,通過superClass指針找到他的父類,從superClass開始搜索,最終結果是差不多的~
題目來源: