Runtime經典面試題(附答案)

minjing_lin發表於2019-04-21

上篇文章:Runtime在工作中的運用

1.objc在向一個物件傳送訊息時,發生了什麼?

objc在向一個物件傳送訊息時,runtime會根據物件的isa指標找到該物件實際所屬的類,然後在該類中的方法列表以及其父類方法列表中尋找方法執行,如果一直到根類還沒找到,轉向攔截呼叫,走訊息轉發機制,一旦找到 ,就去執行它的實現IMP

詳解:請看Runtime在工作中的運用 第二章Runtime訊息機制;

2.objc中向一個nil物件傳送訊息將會發生什麼?

如果向一個nil物件傳送訊息,首先在尋找物件的isa指標時就是0地址返回了,所以不會出現任何錯誤。也不會崩潰。

詳解: 如果一個方法返回值是一個物件,那麼傳送給nil的訊息將返回0(nil);

如果方法返回值為指標型別,其指標大小為小於或者等於sizeof(void*) ,float,double,long double 或者long long的整型標量,傳送給nil的訊息將返回0;

如果方法返回值為結構體,傳送給nil的訊息將返回0。結構體中各個欄位的值將都是0;

如果方法的返回值不是上述提到的幾種情況,那麼傳送給nil的訊息的返回值將是未定義的。

3.objc中向一個物件傳送訊息[obj foo]和objc_msgSend()函式之間有什麼關係?

在objc編譯時,[obj foo] 會被轉意為:objc_msgSend(obj, @selector(foo));

詳解:請看Runtime在工作中的運用 第二章Runtime訊息機制;

4.什麼時候會報unrecognized selector的異常?

objc在向一個物件傳送訊息時,runtime庫會根據物件的isa指標找到該物件實際所屬的類,然後在該類中的方法列表以及其父類方法列表中尋找方法執行,如果,在最頂層的父類中依然找不到相應的方法時,會進入訊息轉發階段,如果訊息三次轉發流程仍未實現,則程式在執行時會掛掉並丟擲異常unrecognized selector sent to XXX 。

詳解:請看Runtime在工作中的運用 第三章Runtime方法呼叫流程;

5.能否向編譯後得到的類中增加例項變數?能否向執行時建立的類中新增例項變數?為什麼?

不能向編譯後得到的類中增加例項變數;

能向執行時建立的類中新增例項變數;

1.因為編譯後的類已經註冊在 runtime 中,類結構體中的 objc_ivar_list 例項變數的連結串列和 instance_size 例項變數的記憶體大小已經確定,同時runtime會呼叫 class_setvarlayout 或 class_setWeaklvarLayout 來處理strong weak 引用.所以不能向存在的類中新增例項變數。

2.執行時建立的類是可以新增例項變數,呼叫class_addIvar函式. 但是的在呼叫 objc_allocateClassPair 之後,objc_registerClassPair 之前,原因同上.

6.給類新增一個屬性後,在類結構體裡哪些元素會發生變化?

instance_size :例項的記憶體大小;objc_ivar_list *ivars:屬性列表

7.一個objc物件的isa的指標指向什麼?有什麼作用?

指向他的類物件,從而可以找到物件上的方法

詳解:下圖很好的描述了物件,類,元類之間的關係:

Runtime經典面試題(附答案)

圖中實線是 super_class指標,虛線是isa指標。

  1. Root class (class)其實就是NSObject,NSObject是沒有超類的,所以Root class(class)的superclass指向nil。
  2. 每個Class都有一個isa指標指向唯一的Meta class
  3. Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一個迴路。
  4. 每個Meta class的isa指標都指向Root class (meta)。

8.[self class] 與 [super class]

下面的程式碼輸出什麼?

   @implementation Son : Father
   - (id)init
   {
       self = [super init];
       if (self) {
           NSLog(@"%@", NSStringFromClass([self class]));
           NSLog(@"%@", NSStringFromClass([super class]));
       }
       return self;
   }
   @end
複製程式碼

NSStringFromClass([self class]) = Son NSStringFromClass([super class]) = Son

詳解:這個題目主要是考察關於 Objective-C 中對 self 和 super 的理解。

self 是類的隱藏引數,指向當前呼叫方法的這個類的例項;

super 本質是一個編譯器標示符,和 self 是指向的同一個訊息接受者。不同點在於:super 會告訴編譯器,當呼叫方法時,去呼叫父類的方法,而不是本類中的方法。

當使用 self 呼叫方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;而當使用 super 時,則從父類的方法列表中開始找。然後呼叫父類的這個方法。

在呼叫[super class]的時候,runtime會去呼叫objc_msgSendSuper方法,而不是objc_msgSend

OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};
複製程式碼

在objc_msgSendSuper方法中,第一個引數是一個objc_super的結構體,這個結構體裡面有兩個變數,一個是接收訊息的receiver,一個是當前類的父類super_class。

objc_msgSendSuper的工作原理應該是這樣的: 從objc_super結構體指向的superClass父類的方法列表開始查詢selector,找到後以objc->receiver去呼叫父類的這個selector。注意,最後的呼叫者是objc->receiver,而不是super_class!

那麼objc_msgSendSuper最後就轉變成:

// 注意這裡是從父類開始msgSend,而不是從本類開始
objc_msgSend(objc_super->receiver, @selector(class))

/// Specifies an instance of a class.  這是類的一個例項
    __unsafe_unretained id receiver;   


// 由於是例項呼叫,所以是減號方法
- (Class)class {
    return object_getClass(self);
}
複製程式碼

由於找到了父類NSObject裡面的class方法的IMP,又因為傳入的入參objc_super->receiver = self。self就是son,呼叫class,所以父類的方法class執行IMP之後,輸出還是son,最後輸出兩個都一樣,都是輸出son。

9.runtime如何通過selector找到對應的IMP地址?

每一個類物件中都一個方法列表,方法列表中記錄著方法的名稱,方法實現,以及引數型別,其實selector本質就是方法名稱,通過這個方法名稱就可以在方法列表中找到對應的方法實現.

10._objc_msgForward函式是做什麼的,直接呼叫它將會發生什麼?

_objc_msgForward是 IMP 型別,用於訊息轉發的:當向一個物件傳送一條訊息,但它並沒有實現的時候,_objc_msgForward會嘗試做訊息轉發。

詳解:_objc_msgForward在進行訊息轉發的過程中會涉及以下這幾個方法:

  1. resolveInstanceMethod:方法 (或 resolveClassMethod:)。
  2. forwardingTargetForSelector:方法
  3. methodSignatureForSelector:方法
  4. forwardInvocation:方法
  5. doesNotRecognizeSelector: 方法

具體請見:請看Runtime在工作中的運用 第三章Runtime方法呼叫流程;

11. runtime如何實現weak變數的自動置nil?知道SideTable嗎?

runtime 對註冊的類會進行佈局,對於 weak 修飾的物件會放入一個 hash 表中。 用 weak 指向的物件記憶體地址作為 key,當此物件的引用計數為0的時候會 dealloc,假如 weak 指向的物件記憶體地址是a,那麼就會以a為鍵, 在這個 weak 表中搜尋,找到所有以a為鍵的 weak 物件,從而設定為 nil。

更細一點的回答:

1.初始化時:runtime會呼叫objc_initWeak函式,初始化一個新的weak指標指向物件的地址。
2.新增引用時:objc_initWeak函式會呼叫objc_storeWeak() 函式, objc_storeWeak() 的作用是更新指標指向,建立對應的弱引用表。
3.釋放時,呼叫clearDeallocating函式。clearDeallocating函式首先根據物件地址獲取所有weak指標地址的陣列,然後遍歷這個陣列把其中的資料設為nil,最後把這個entry從weak表中刪除,最後清理物件的記錄。

SideTable結構體是負責管理類的引用計數表和weak表,

詳解:參考自《Objective-C高階程式設計》一書 1.初始化時:runtime會呼叫objc_initWeak函式,初始化一個新的weak指標指向物件的地址。

{
    NSObject *obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
}
複製程式碼

當我們初始化一個weak變數時,runtime會呼叫 NSObject.mm 中的objc_initWeak函式。

// 編譯器的模擬程式碼
 id obj1;
 objc_initWeak(&obj1, obj);
/*obj引用計數變為0,變數作用域結束*/
 objc_destroyWeak(&obj1);
複製程式碼

通過objc_initWeak函式初始化“附有weak修飾符的變數(obj1)”,在變數作用域結束時通過objc_destoryWeak函式釋放該變數(obj1)。

2.新增引用時:objc_initWeak函式會呼叫objc_storeWeak() 函式, objc_storeWeak() 的作用是更新指標指向,建立對應的弱引用表。

objc_initWeak函式將“附有weak修飾符的變數(obj1)”初始化為0(nil)後,會將“賦值物件”(obj)作為引數,呼叫objc_storeWeak函式。

obj1 = 0;
obj_storeWeak(&obj1, obj);
複製程式碼

也就是說:

weak 修飾的指標預設值是 nil (在Objective-C中向nil傳送訊息是安全的)

然後obj_destroyWeak函式將0(nil)作為引數,呼叫objc_storeWeak函式。

objc_storeWeak(&obj1, 0);
複製程式碼

前面的原始碼與下列原始碼相同。

// 編譯器的模擬程式碼
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用計數變為0,被置nil ... */
objc_storeWeak(&obj1, 0);
複製程式碼

objc_storeWeak函式把第二個引數的賦值物件(obj)的記憶體地址作為鍵值,將第一個引數__weak修飾的屬性變數(obj1)的記憶體地址註冊到 weak 表中。如果第二個引數(obj)為0(nil),那麼把變數(obj1)的地址從weak表中刪除。

由於一個物件可同時賦值給多個附有__weak修飾符的變數中,所以對於一個鍵值,可註冊多個變數的地址。

可以把objc_storeWeak(&a, b)理解為:objc_storeWeak(value, key),並且當key變nil,將value置nil。在b非nil時,a和b指向同一個記憶體地址,在b變nil時,a變nil。此時向a傳送訊息不會崩潰:在Objective-C中向nil傳送訊息是安全的。

3.釋放時,呼叫clearDeallocating函式。clearDeallocating函式首先根據物件地址獲取所有weak指標地址的陣列,然後遍歷這個陣列把其中的資料設為nil,最後把這個entry從weak表中刪除,最後清理物件的記錄。

當weak引用指向的物件被釋放時,又是如何去處理weak指標的呢?當釋放物件時,其基本流程如下:

1.呼叫objc_release
2.因為物件的引用計數為0,所以執行dealloc
3.在dealloc中,呼叫了_objc_rootDealloc函式
4.在_objc_rootDealloc中,呼叫了object_dispose函式
5.呼叫objc_destructInstance
6.最後呼叫objc_clear_deallocating

物件被釋放時呼叫的objc_clear_deallocating函式:

1.從weak表中獲取廢棄物件的地址為鍵值的記錄
2.將包含在記錄中的所有附有 weak修飾符變數的地址,賦值為nil
3.將weak表中該記錄刪除
4.從引用計數表中刪除廢棄物件的地址為鍵值的記錄

總結:

其實Weak表是一個hash(雜湊)表,Key是weak所指物件的地址,Value是weak指標的地址(這個地址的值是所指物件指標的地址)陣列。

12.isKindOfClass 與 isMemberOfClass

下面程式碼輸出什麼?

@interface Sark : NSObject
@end
@implementation Sark
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
        BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
        BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
        BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
        NSLog(@"%d %d %d %d", res1, res2, res3, res4);
    }
    return 0;
}
複製程式碼

1000

詳解:

isKindOfClass中有一個迴圈,先判斷class是否等於meta class,不等就繼續迴圈判斷是否等於meta classsuper class,不等再繼續取super class,如此迴圈下去。

[NSObject class]執行完之後呼叫isKindOfClass,第一次判斷先判斷NSObjectNSObjectmeta class是否相等,之前講到meta class的時候放了一張很詳細的圖,從圖上我們也可以看出,NSObjectmeta class與本身不等。接著第二次迴圈判斷NSObjectmeta classsuperclass是否相等。還是從那張圖上面我們可以看到:Root class(meta)superclass就是 Root class(class),也就是NSObject本身。所以第二次迴圈相等,於是第一行res1輸出應該為YES。

同理,[Sark class]執行完之後呼叫isKindOfClass,第一次for迴圈,Sark的Meta Class[Sark class]不等,第二次for迴圈,Sark Meta Classsuper class 指向的是 NSObject Meta Class, 和Sark Class不相等。第三次for迴圈,NSObject Meta Classsuper class指向的是NSObject Class,和 Sark Class 不相等。第四次迴圈,NSObject Classsuper class 指向 nil, 和 Sark Class不相等。第四次迴圈之後,退出迴圈,所以第三行的res3輸出為NO。

isMemberOfClass的原始碼實現是拿到自己的isa指標和自己比較,是否相等。 第二行isa 指向 NSObjectMeta Class,所以和 NSObject Class不相等。第四行,isa指向Sark的Meta Class,和Sark Class也不等,所以第二行res2和第四行res4都輸出NO。

13.使用runtime Associate方法關聯的物件,需要在主物件dealloc的時候釋放麼?

無論在MRC下還是ARC下均不需要,被關聯的物件在生命週期內要比物件本身釋放的晚很多,它們會在被 NSObject -dealloc 呼叫的object_dispose()方法中釋放。

詳解:

1、呼叫 -release :引用計數變為零
物件正在被銷燬,生命週期即將結束. 
不能再有新的 __weak 弱引用,否則將指向 nil.
呼叫 [self dealloc]

2、 父類呼叫 -dealloc 
繼承關係中最直接繼承的父類再呼叫 -dealloc 
如果是 MRC 程式碼 則會手動釋放例項變數們(iVars)
繼承關係中每一層的父類 都再呼叫 -dealloc

>3、NSObject 調 -dealloc 
只做一件事:呼叫 Objective-C runtime 中object_dispose() 方法

>4. 呼叫 object_dispose()
為 C++ 的例項變數們(iVars)呼叫 destructors
為 ARC 狀態下的 例項變數們(iVars) 呼叫 -release 
解除所有使用 runtime Associate方法關聯的物件 
解除所有 __weak 引用 
呼叫 free()
複製程式碼

14. 什麼是method swizzling(俗稱黑魔法)

簡單說就是進行方法交換

詳解:請看Runtime在工作中的運用 第五章Runtime方法交換;

在Objective-C中呼叫一個方法,其實是向一個物件傳送訊息,查詢訊息的唯一依據是selector的名字。利用Objective-C的動態特性,可以實現在執行時偷換selector對應的方法實現,達到給方法掛鉤的目的。

每個類都有一個方法列表,存放著方法的名字和方法實現的對映關係,selector的本質其實就是方法名,IMP有點類似函式指標,指向具體的Method實現,通過selector就可以找到對應的IMP。

換方法的幾種實現方式

  • 利用 method_exchangeImplementations 交換兩個方法的實現
  • 利用 class_replaceMethod 替換方法的實現
  • 利用 method_setImplementation 來直接設定某個方法的IMP

Runtime經典面試題(附答案)

15.Compile Error / Runtime Crash / NSLog…?

下面的程式碼會?Compile Error / Runtime Crash / NSLog…?

@interface NSObject (Sark)
+ (void)foo;
- (void)foo;
@end

@implementation NSObject (Sark)
- (void)foo {
    NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end

// 測試程式碼
[NSObject foo];
[[NSObject new] performSelector:@selector(foo)];
複製程式碼

IMP: -[NSObject(Sark) foo] ,全都正常輸出,編譯和執行都沒有問題。

詳解:

這道題和上一道題很相似,第二個呼叫肯定沒有問題,第一個呼叫後會從元類中查詢方法,然而方法並不在元類中,所以找元類的superclass。方法定義在是NSObjectCategory,由於NSObject的物件模型比較特殊,元類的superclass是類物件,所以從類物件中找到了方法並呼叫。

感謝:

霜神、iOS程式犭袁、sunnyxx

相關文章