歡迎光臨
每天分享高質量文章

一道高階iOS面試題(runtime方向)

作者:小蠢驢打程式碼
連結:https://www.jianshu.com/p/70a567cbd4ce
面試驅動技術合集(初中級iOS開發),關註倉庫,及時獲取更新 Interview-series

 

說到iOS,要麼公司規模比較小,<=3人,不需要面試。

 

其他的,大機率要讓你刀槍棍棒十八般武藝都拿出來耍耍。

 

而其中,但凡敵軍陣營中有iOSer的,又極大機率會考到 Runtime 的知識點。

 

以下,是一題 sunnyxx的一道 runtime 考題,給大夥練練手,如果掌握了,Runtime層面的初中級問題應該都不在話下~

題目來襲:

//MNPerson
@interface MNPerson : NSObject

@property (nonatomiccopy)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);
}

 

  1. 要列印的 _name  成員變數,其實是透過 self -> 去查詢;
  2. 這裡的 self,就是函式呼叫者;
  3. [(__bridge id)obj print]; 即透過 obj 開始找;
  4. 而找 _name ,是透過指標地址查詢,找得MNPerson_IMPL 結構體
  5. 因為這裡的 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開始搜尋,最終結果是差不多的~

 

友情演出:小馬哥MJ

 

題目來源:

  • 神經病院入學考試
  • runtime訊息機制理解

已同步到看一看
贊(0)

分享創造快樂