1. 程式人生 > >iOS面試題整理(下) 仔細研讀受益匪淺

iOS面試題整理(下) 仔細研讀受益匪淺

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

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

我們可以這樣建立一個_objc_msgForward物件:

IMP msgForwardIMP = _objc_msgForward;

上篇中的《objc中向一個物件傳送訊息[obj foo]objc_msgSend()函式之間有什麼關係?》曾提到objc_msgSend在“訊息傳遞”中的作用。在“訊息傳遞”過程中,objc_msgSend

的動作比較清晰:首先在 Class 中的快取查詢 IMP (沒快取則初始化快取),如果沒找到,則向父類的 Class 查詢。如果一直查詢到根類仍舊沒有實現,則用_objc_msgForward函式指標代替 IMP 。最後,執行這個 IMP 。

Objective-C執行時是開源的,所以我們可以看到它的實現。開啟 Apple Open Source 裡Mac程式碼裡的obj包 下載一個最新版本,找到 objc-runtime-new.mm,進入之後搜尋_objc_msgForward

enter image description here

裡面有對_objc_msgForward的功能解釋:

enter image description here

/***********************************************************************
* lookUpImpOrForward. * The standard IMP lookup. * initialize==NO tries to avoid +initialize (but sometimes fails) * cache==NO skips optimistic unlocked lookup (but uses cache elsewhere) * Most callers should use initialize==YES and cache==YES. * inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster. * May return _objc_msgForward_impcache. IMPs destined for external use * must be converted to _objc_msgForward or _objc_msgForward_stret. * If you don't want forwarding at all, use lookUpImpOrNil() instead. **********************************************************************/

對 objc-runtime-new.mm檔案裡與_objc_msgForward有關的三個函式使用虛擬碼展示下:

//  objc-runtime-new.mm 檔案裡與 _objc_msgForward 有關的三個函式使用虛擬碼展示
//  Created by https://github.com/ChenYilong
//  Copyright (c)  微博@iOS程式犭袁(http://weibo.com/luohanchenyilong/). All rights reserved.
//  同時,這也是 obj_msgSend 的實現過程

id objc_msgSend(id self, SEL op, ...) {
    if (!self) return nil;
    IMP imp = class_getMethodImplementation(self->isa, SEL op);
    imp(self, op, ...); //呼叫這個函式,虛擬碼...
}

//查詢IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
    if (!cls || !sel) return nil;
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) return _objc_msgForward; //_objc_msgForward 用於訊息轉發
    return imp;
}

IMP lookUpImpOrNil(Class cls, SEL sel) {
    if (!cls->initialize()) {
        _class_initialize(cls);
    }

    Class curClass = cls;
    IMP imp = nil;
    do { //先查快取,快取沒有時重建,仍舊沒有則向父類查詢
        if (!curClass) break;
        if (!curClass->cache) fill_cache(cls, curClass);
        imp = cache_getImp(curClass, sel);
        if (imp) break;
    } while (curClass = curClass->superclass);

    return imp;
}

雖然Apple沒有公開_objc_msgForward的實現原始碼,但是我們還是能得出結論:

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

上篇中的《objc中向一個物件傳送訊息[obj foo]objc_msgSend()函式之間有什麼關係?》曾提到objc_msgSend在“訊息傳遞”中的作用。在“訊息傳遞”過程中,objc_msgSend的動作比較清晰:首先在 Class 中的快取查詢 IMP (沒快取則初始化快取),如果沒找到,則向父類的 Class 查詢。如果一直查詢到根類仍舊沒有實現,則用_objc_msgForward函式指標代替 IMP 。最後,執行這個 IMP 。

為了展示訊息轉發的具體動作,這裡嘗試向一個物件傳送一條錯誤的訊息,並檢視一下_objc_msgForward是如何進行轉發的。

首先開啟除錯模式、打印出所有執行時傳送的訊息: 可以在程式碼裡執行下面的方法:

(void)instrumentObjcMessageSends(YES);

或者斷點暫停程式執行,並在 gdb 中輸入下面的命令:

call (void)instrumentObjcMessageSends(YES)

以第二種為例,操作如下所示:

enter image description here

之後,執行時傳送的所有訊息都會列印到/tmp/msgSend-xxxx檔案裡了。

終端中輸入命令前往:

open /private/tmp

enter image description here

可能看到有多條,找到最新生成的,雙擊開啟

//
//  main.m
//  CYLObjcMsgForwardTest
//
//  Created by http://weibo.com/luohanchenyilong/.
//  Copyright (c) 2015年 微博@iOS程式犭袁. All rights reserved.
//

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "CYLTest.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        CYLTest *test = [[CYLTest alloc] init];
        [test performSelector:(@selector(iOS程式犭袁))];
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

enter image description here

你可以在/tmp/msgSend-xxxx(我這一次是/tmp/msgSend-9805)檔案裡,看到打印出來:

enter image description here

+ CYLTest NSObject initialize
+ CYLTest NSObject alloc
- CYLTest NSObject init
- CYLTest NSObject performSelector:
+ CYLTest NSObject resolveInstanceMethod:
+ CYLTest NSObject resolveInstanceMethod:
- CYLTest NSObject forwardingTargetForSelector:
- CYLTest NSObject forwardingTargetForSelector:
- CYLTest NSObject methodSignatureForSelector:
- CYLTest NSObject methodSignatureForSelector:
- CYLTest NSObject class
- CYLTest NSObject doesNotRecognizeSelector:
- CYLTest NSObject doesNotRecognizeSelector:
- CYLTest NSObject class

結合《NSObject官方文件》,排除掉 NSObject 做的事,剩下的就是_objc_msgForward訊息轉發做的幾件事:

  1. 呼叫resolveInstanceMethod:方法 (或 resolveClassMethod:)。允許使用者在此時為該 Class 動態新增實現。如果有實現了,則呼叫並返回YES,那麼重新開始objc_msgSend流程。這一次物件會響應這個選擇器,一般是因為它已經呼叫過class_addMethod。如果仍沒實現,繼續下面的動作。

  2. 呼叫forwardingTargetForSelector:方法,嘗試找到一個能響應該訊息的物件。如果獲取到,則直接把訊息轉發給它,返回非 nil 物件。否則返回 nil ,繼續下面的動作。注意,這裡不要返回 self ,否則會形成死迴圈。

  3. 呼叫methodSignatureForSelector:方法,嘗試獲得一個方法簽名。如果獲取不到,則直接呼叫doesNotRecognizeSelector丟擲異常。如果能獲取,則返回非nil:建立一個 NSlnvocation 並傳給forwardInvocation:

  4. 呼叫forwardInvocation:方法,將第3步獲取到的方法簽名包裝成 Invocation 傳入,如何處理就在這裡面了,並返回非ni。

  5. 呼叫doesNotRecognizeSelector: ,預設的實現是丟擲異常。如果第3步沒能獲得一個方法簽名,執行該步驟。

上面前4個方法均是模板方法,開發者可以override,由 runtime 來呼叫。最常見的實現訊息轉發:就是重寫方法3和4,吞掉一個訊息或者代理給其他物件都是沒問題的

也就是說_objc_msgForward在進行訊息轉發的過程中會涉及以下這幾個方法:

  1. resolveInstanceMethod:方法 (或 resolveClassMethod:)。

  2. forwardingTargetForSelector:方法

  3. methodSignatureForSelector:方法

  4. forwardInvocation:方法

  5. doesNotRecognizeSelector: 方法

為了能更清晰地理解這些方法的作用,git倉庫裡也給出了一個Demo,名稱叫“ _objc_msgForward_demo ”,可執行起來看看。

下面回答下第二個問題“直接_objc_msgForward呼叫它將會發生什麼?”

直接呼叫_objc_msgForward是非常危險的事,如果用不好會直接導致程式Crash,但是如果用得好,能做很多非常酷的事。

就好像跑酷,幹得好,叫“耍酷”,幹不好就叫“作死”。

正如前文所說:

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

如何呼叫_objc_msgForward? _objc_msgForward隸屬 C 語言,有三個引數 :

-- _objc_msgForward引數 型別
1. 所屬物件 id型別
2. 方法名 SEL型別
3. 可變引數 可變引數型別

首先了解下如何呼叫 IMP 型別的方法,IMP型別是如下格式:

為了直觀,我們可以通過如下方式定義一個 IMP型別 :

typedef void (*voidIMP)(id, SEL, ...)

一旦呼叫_objc_msgForward,將跳過查詢 IMP 的過程,直接觸發“訊息轉發”,

如果呼叫了_objc_msgForward,即使這個物件確實已經實現了這個方法,你也會告訴objc_msgSend

“我沒有在這個物件裡找到這個方法的實現”

想象下objc_msgSend會怎麼做?通常情況下,下面這張圖就是你正常走objc_msgSend過程,和直接呼叫_objc_msgForward的前後差別:

enter image description here

有哪些場景需要直接呼叫_objc_msgForward?最常見的場景是:你想獲取某方法所對應的NSInvocation物件。舉例說明:

JSPatch (Github 連結)就是直接呼叫_objc_msgForward來實現其核心功能的:

JSPatch 以小巧的體積做到了讓JS呼叫/替換任意OC方法,讓iOS APP具備熱更新的能力。

作者的博文《JSPatch實現原理詳解》詳細記錄了實現原理,有興趣可以看下。

26. runtime如何實現weak變數的自動置nil?

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

上篇中的《runtime 如何實現 weak 屬性》有論述。(注:在上篇的《使用runtime Associate方法關聯的物件,需要在主物件dealloc的時候釋放麼?》裡給出的“物件的記憶體銷燬時間表”也提到__weak引用的解除時間。)

我們可以設計一個函式(虛擬碼)來表示上述機制:

objc_storeWeak(&a, b)函式:

objc_storeWeak函式把第二個引數--賦值物件(b)的記憶體地址作為鍵值key,將第一個引數--weak修飾的屬性變數(a)的記憶體地址(&a)作為value,註冊到 weak 表中。如果第二個引數(b)為0(nil),那麼把變數(a)的記憶體地址(&a)從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傳送訊息是安全的。

而如果a是由assign修飾的,則: 在b非nil時,a和b指向同一個記憶體地址,在b變nil時,a還是指向該記憶體地址,變野指標。此時向a傳送訊息極易崩潰。

下面我們將基於objc_storeWeak(&a, b)函式,使用虛擬碼模擬“runtime如何實現weak屬性”:

// 使用虛擬碼模擬:runtime如何實現weak屬性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong

 id obj1;
 objc_initWeak(&obj1, obj);
/*obj引用計數變為0,變數作用域結束*/
 objc_destroyWeak(&obj1);

下面對用到的兩個方法objc_initWeakobjc_destroyWeak做下解釋:

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

下面分別介紹下方法的內部實現:

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

obj1 = 0obj_storeWeak(&obj1, obj);

也就是說:

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

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

objc_storeWeak(&obj1, 0);

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

// 使用虛擬碼模擬:runtime如何實現weak屬性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong

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表中刪除。

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

  • 不能向編譯後得到的類中增加例項變數;
  • 能向執行時建立的類中新增例項變數;

解釋下:

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

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

28. runloop和執行緒有什麼關係?

總的說來,Run loop,正如其名,loop表示某種迴圈,和run放在一起就表示一直在執行著的迴圈。實際上,run loop和執行緒是緊密相連的,可以這樣說run loop是為了執行緒而生,沒有執行緒,它就沒有存在的必要。Run loops是執行緒的基礎架構部分, Cocoa 和 CoreFundation 都提供了 run loop 物件方便配置和管理執行緒的 run loop (以下都以 Cocoa 為例)。每個執行緒,包括程式的主執行緒( main thread )都有與之相應的 run loop 物件。

runloop 和執行緒的關係: