iOS底層學習 - Runtime之磚廠面試答疑

經過對類,Runtime等底層的相關探索,對原理已經有了掌握,本章經過幾個面試題來加深印象,會保持持續更新。面試

先上幾篇原理文章,對面試題的理解會更深入緩存

傳送門☞iOS底層學習 - Runtime之方法消息的前世此生(一)bash

傳送門☞iOS底層學習 - Runtime之方法消息的前世此生(二)markdown

1.什麼是Runtime?

答:是由C 和C++ 彙編 實現的⼀套API,爲OC語⾔加⼊了⾯向對象,運⾏時的功能。平時編寫的OC代碼,在程序運⾏過程當中,其實最終會轉換成Runtime的C語⾔代 碼,RuntimeObjective-C 的幕後⼯做者。app

好比:將數據類型的肯定由編譯時推遲到了運⾏時,比較典型的就是類的rorw屬性。ro在編譯期就肯定好(read-only),而rw是運行時才肯定,能夠進行修改(read-write)。像下面的例子就比較典型👇框架

2.方法的本質是什麼?

方法的本質就是消息的發送,就底層_objc_msgSennd方法尋找方法IMP的過程,主要經歷瞭如下幾個步驟:ide

  1. 快速查找流程:經過彙編(objc_msgSend)查找cache_t中緩存的消息
  2. 慢速查找流程:經過C代碼函數lookUpImpOrForward遞歸查找當前類和父類的rwmethodlist的方法
  3. 動態方法解析:查找不到方法後進入此流程,經過調用和實現resolveInstanceMethod方法,來實現消息動態處理
  4. 消息快速轉發:無方法無動態解析進入此流程,經過CoreFoundation框架來觸發消息轉發流程,forwardingTargetForSelector實現快速轉發,其餘類可實現處理方法
  5. 消息慢速轉發:經過實現methodSignatureForSelector方法,來獲取到方法的簽名,從在生成相對應的invocation,經過forwardInvocation來對invocation進行處理,通常處置崩潰都在此處理
  6. 未找到消息:沒法找到IMP,形成崩潰,打印log

3.sel是什麼?IMP是什麼?二者之間的關係⼜是什麼?

SEL是方法編號,也是方法名,在dyld加載鏡像帶內存時,經過_read_image方法加載到內存的表中了函數

IMP 就是咱們函數實現指針 ,找IMP就是找函數的過程oop

SEL就至關於書本的⽬錄 tittle,post

IMP 就是書本的⻚碼,

函數就是具體頁碼對應的實現內容

查找具體的函數就是想看這本書⾥⾯具體篇章的內容

  1. 咱們⾸先知道想看什麼 ~ tittle (sel)
  2. 根據⽬錄對應的⻚碼 (imp)
  3. 翻到具體的內容

4.可否向運⾏時建立的類中添加實例變量?

不能。

由於咱們編譯好的實例變量存儲的位置在ro,⼀旦編譯完成,內存結構就徹底肯定 就⽆法修改,只能修改rw中的方法或者能夠經過關聯對象的方式來添加屬性

關聯對象添加的主要步驟以下:

  1. objc_setAssociatedObject設置set方法:找到關聯對象的總哈希表,而後經過指針地址找到該類的哈希表,而後經過key值進行存儲
  2. objc_getAssociatedObject設置get方法:和set方法同樣查詢表,找到值
  3. 在類的dealloc會清除關聯對象的哈希表

5.isKindOfClassisMemberOfClass區別

原理探究

先上例子🌰

1.第一個

BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];     
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
複製代碼

👆上述Log的打印結果爲1,0,0,0

2.第二個

BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       // 1
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     // 1
BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]];       // 1
BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     // 1
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
複製代碼

👆上述Log的打印結果爲1,1,1,1

相信對於第二個例子,你們都使用的很是熟練。咱們發現兩個例子的主要區別在於消息接受者是實例對象仍是類對象,打印結果,咱們看源碼探究

既然[NSObject class]類也能夠調用這兩個方法,說明這兩個方法是有對應的類方法和實例方法的,只不過咱們平時不適用類方法而已😂

/*********************************************************************** * object_getClass. * Locking: None. If you add locking, tell gdb (rdar://7516456). **********************************************************************/
Class object_getClass(id obj) {
    if (obj) return obj->getIsa();
    else return Nil;
}
複製代碼
✅//object_getClass()取得的是對象的isa指針指向的對象,也就是判斷傳入的類對象的元類對象是否與傳入的這個對象相等,因此這個cls應該是元類對象纔有可能相等
 + (BOOL)isMemberOfClass:(Class)cls {
 return object_getClass((id)self) == cls;
 }

 ✅//判斷傳入的實例對象的類對象是否與傳入的對象相等,因此cls只有多是類對象纔有可能相等
 - (BOOL)isMemberOfClass:(Class)cls {
 return [self class] == cls;
 }

✅//循環判斷傳入的類對象的元類對象及其父類的元類對象是否等於傳入的cls
 + (BOOL)isKindOfClass:(Class)cls {
 for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
 if (tcls == cls) return YES;
 }
 return NO;
 }

✅//循環判斷實例對象的父類的類對象是否等於傳入的對象cls,也就是判斷實例對象是不是cls及其子類的一種
 - (BOOL)isKindOfClass:(Class)cls {
 for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
 if (tcls == cls) return YES;
 }
 return NO;
 }
複製代碼

經過這個兩個方法的源碼咱們能夠知道,

  • isMemberOfClass:是檢測方法調用者對象的類是否等於傳入的這個類。
  • isKindOfClass:是判斷方法調用者對象的類是否等於傳入的這個類或者其子類。

小結

還有一個適用於這四個方法的一點是,若是方法調用者是實例對象,那麼傳入的就應該是類對象;若是方法調用者是類對象,那麼傳入的就應該是元類對象。

分析BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];爲啥打印是1呢?

答:[NSObject class]類對象調用isKindOfClass代表其元類會遞歸判斷是否等於當前類或者其父類,咱們知道NSObject的元類爲根元類,根據繼承鏈關係根元類的父類即爲NSObject類對象,即[NSObject class],因此相等

6.[self class]和[super class]的區別以及原理分析

原理探究

建立一個Student類繼承子Person類,下面代碼打印出什麼

NSLog(@"[self class] = %@", [self class]);
NSLog(@"[super class] = %@", [super class]);
NSLog(@"[self superclass] = %@", [self superclass]);
NSLog(@"[super superclass] = %@", [super superclass]);
複製代碼

先上正確答案:

2020-01-17 15:54:02.224686+0800 TEST[8409:174143] [self class] = Student
2020-01-17 15:54:02.224922+0800 TEST[8409:174143] [super class] = Student
2020-01-17 15:54:02.225040+0800 TEST[8409:174143] [self superclass] = Person
2020-01-17 15:54:02.225922+0800 TEST[8409:174143] [super superclass] = Person
複製代碼

咱們發現第二個和第四個和咱們猜測的貌似不太同樣,並非Person,先上源碼看一下第一個和第三個的理解

/*******************************************************
✅ //經過對象的isa指針獲取類的類對象
Class object_getClass(id obj)
 {
 if (obj) return obj->getIsa();
 else return Nil;
 }

 + (Class)class {
 return self;
 }

 - (Class)class {
 return object_getClass(self);
 }

 + (Class)superclass {
 return self->superclass;
 }

 - (Class)superclass {
 return [self class]->superclass;
 }

Class class_getSuperclass(Class cls)
{
    if (!cls) return nil;
    return cls->superclass;
}
******************************************************/
複製代碼

咱們知道,這裏方法中的self都是指消息的接受者,在問題中就表示Student類,根據以上源碼,第一個和第三個沒啥疑問,都是尋找Studentisa和父類,那麼super調用時,有啥不一樣,咱們須要先知道super的本質

經過clang編譯代碼,咱們能夠發現,底層調用時super調用的方法不是magSend,而是objc_msgSendSuper,傳入了一個super的結構體和方法,而super的結構體以下

objc_msgSendSuper(object ,superclass, @selector(class))
複製代碼
struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};
複製代碼

兩個參數分別爲消息的接受者和父類,在這裏Student即爲消息接受者,Person爲父類.

咱們知道消息發送的時候,慢速查找流程是須要從自身遞歸查找到NSObject的,而objc_msgSendSuper就表示直接從消息接受者的父類開始遞歸查找,跳過了自己的方法列表,這樣查找的速度能夠更快

因此調用[super class][super superclass]本質上消息的接受者仍是self,即Student類,因爲class方法的實現,實際上是在基類NSObject中的,因此不論是從Student類方法列表開始查詢,仍是從父類Person方法列表查詢,最終都會走到基類中的class方法,而此方法根據源碼就是尋找消息接受者的isasuperclass,因此出現上述的打印結果

小結

[self class] 就是發送消息objc_msgSend,消息接受者是 self,⽅法編號:class

[super class] 本質就是objc_msgSendSuper, 消息的接受者仍是 self ⽅法編號:class

只是objc_msgSendSuper 會更快 直接跳過 self 的查找,可是都會走到NSObject基類的實現方法中,可是都是以self爲接受者

7.Runtime是如何實現weak的,爲何能夠⾃動置nil?

主要總結以下:

  1. 經過SideTable找到咱們的weak_table

  2. weak_table 根據referent 找到或者建立 weak_entry_t

  3. 而後append_referrer(entry, referrer)將新弱引⽤的對象加進去entry

  4. 最後weak_entry_insertentry加⼊到咱們的weak_table

  5. 在類dealloc時,會根據插入的步驟找到對應的弱引用,並置爲nil

關於weak的相關知識作了單獨的總結,詳情能夠看下方文章👇

iOS底層學習 - 內存管理之weak原理探究

8.Method Swizzling的坑與應⽤

關於Method Swizzling方法交換的總結,詳情能夠看下面的文章:

iOS底層學習 - Runtime之Method Swizzling黑魔法

9.壓軸大題:下列代碼可否運行,打印結果是什麼?

題目

***********************LGPerson***************************
@interface LGPerson : NSObject
@property (nonatomic, copy)NSString *name;
//@property (nonatomic, copy)NSString *subject;
//@property (nonatomic)int age;

- (void)print;
@end

@implementation LGPerson
- (void)print{
    NSLog(@"NB %s - %@",__func__,self.name);
}
@end

**************************調用*****************************
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *tem = @"WY";
    id cls = [LGPerson class];
    void *obj= &cls;
    [(__bridge id)obj print];
    
    //    LGPerson *person = [LGPerson alloc];
    //    [person print];
    
}

複製代碼

運行結果

2020-01-20 17:40:15.322404+0800 LGTest[86411:20872420] NB -[LGPerson saySomething] - KC
複製代碼

相信看到這個結果你們都是和我同樣一臉懵逼。下面就分爲2個問題,咱們來研究一下

問題1: 可否運行

經過結果也看到了,這段代碼是能夠運行的。而且成功調用了[LGPerson saySomething]方法,咱們首先看爲什麼能成功調用,它和咱們註釋的普通的實例對象調用爲什麼能同樣。

首先來看一下普通方法調用的時序:

  • 實例對象p的指針 --> 實例對象p的isa --> [LGPerson class]類地址

如圖所示:

接着來分析代碼中的執行時序:

  1. id cls = [LGPerson class];獲取到類對象指針
  2. void *obj= &cls;獲取到指向該類對象cls的對象obj
  3. [(__bridge id)obj print];消息發送

按照上面的時序的結構總結一下,就是:

  • 指針obj --> 指針cls --> [LGPerson class]類地址

因此從本質上來講,最後消息發送的時候,都是根據isa獲取到類的內存空間,而後再方法列表中查找IMP。而obj指向的是一個cls,可是cls正巧也是指向類的內存空間的,因此一樣也能夠找到方法列表中的print方法,進行調用

問題2: 打印內容及爲何會打印這個結果

經過運行結果咱們能夠看到,方法調用self.name居然打印出了VC中的NSString的臨時變量的值,那麼爲何會出現這個結果呢?

咱們知道任何OC方法的底層都是一個C函數,而且函數頭兩個參數是默認參數id selfSEL _cmd,那麼self是誰呢

首先咱們仍是來看一下正常的調用self.name是如何找到的

  • 此時,咱們知道消息發送時的self(消息接受者)指的就是咱們實例化的對象person,而person指向的是實例對象的內存空間首地址,而內存空間首地址是第一個元素isa,佔用8字節,name是第二個元素,也是佔用8個字節。尋找self.name的過程就是指針偏移的過程,由於isa佔用了8個,因此找到name的值時,只須要向後便宜8個字節便可。如圖所示:

那麼咱們再來看一下代碼中時如何找到self.name

經過上面的時序,咱們知道這二者在調用方法上是對等的。這裏消息發送時的self(消息接受者)指的就是objcls指針至關於person指針所指向的實例對象裏面的isa指針,同理,此時指向的類的首地址,可是要找的是name,向下指針偏移8個後,找到了生命的臨時變量的值,因此會打印出來

函數的棧空間簡介

這裏就涉及到了爲何會找到臨時變量的問題,若是去掉了臨時變量,又會打印什麼呢,這裏就有了函數棧空間的做用

棧空間的做用,是用來存放被調用函數其內部所定義的局部變量的。咱們都知道棧的特色是先入後出,因此先存進棧的在底層。因爲方法的調用和super的調用,都會產生局部變量,因此viewdidload的棧示意圖以下:

此時代碼指向的就是圖中橘色obj的首地址,若是沒有臨時變量,便宜8位後,就會指向 self,即當前的 viewcontroller

若是加了一個臨時變量NSString,棧的結構就會變成以下所示,因此會打印它的值

能夠看出此時對象的實例變量獲取即爲void *ivar = &obj + offset(N)

可是咱們指針偏移的offset不正確,獲取不到對應變量的首地址,此時就會出現野指針的狀況,因此千萬不能像代碼中那麼用

參考

iOS底層學習 - OC對象前世此生

Runtime筆記(八)—— 面試題中的Runtime

神經病院 Objective-C

神經病院objc runtime入院考試

相關文章
相關標籤/搜索