iOS面試題整理(下) 仔細研讀受益匪淺
25. _objc_msgForward
函式是做什麼的,直接呼叫它將會發生什麼?
_objc_msgForward
是 IMP 型別,用於訊息轉發的:當向一個物件傳送一條訊息,但它並沒有實現的時候,_objc_msgForward
會嘗試做訊息轉發。
我們可以這樣建立一個_objc_msgForward
物件:
IMP msgForwardIMP = _objc_msgForward;
在上篇中的《objc中向一個物件傳送訊息[obj
foo]
和objc_msgSend()
函式之間有什麼關係?》曾提到objc_msgSend
在“訊息傳遞”中的作用。在“訊息傳遞”過程中,objc_msgSend
_objc_msgForward
函式指標代替 IMP 。最後,執行這個 IMP 。
Objective-C執行時是開源的,所以我們可以看到它的實現。開啟 Apple Open Source
裡Mac程式碼裡的obj包 下載一個最新版本,找到 objc-runtime-new.mm
,進入之後搜尋_objc_msgForward
。
裡面有對_objc_msgForward
的功能解釋:
/************************************************************************ 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)
以第二種為例,操作如下所示:
之後,執行時傳送的所有訊息都會列印到/tmp/msgSend-xxxx
檔案裡了。
終端中輸入命令前往:
open /private/tmp
可能看到有多條,找到最新生成的,雙擊開啟
// // 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])); } }
你可以在/tmp/msgSend-xxxx
(我這一次是/tmp/msgSend-9805
)檔案裡,看到打印出來:
+ 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
訊息轉發做的幾件事:
-
呼叫
resolveInstanceMethod:
方法 (或resolveClassMethod:
)。允許使用者在此時為該 Class 動態新增實現。如果有實現了,則呼叫並返回YES,那麼重新開始objc_msgSend
流程。這一次物件會響應這個選擇器,一般是因為它已經呼叫過class_addMethod
。如果仍沒實現,繼續下面的動作。 -
呼叫
forwardingTargetForSelector:
方法,嘗試找到一個能響應該訊息的物件。如果獲取到,則直接把訊息轉發給它,返回非 nil 物件。否則返回 nil ,繼續下面的動作。注意,這裡不要返回 self ,否則會形成死迴圈。 -
呼叫
methodSignatureForSelector:
方法,嘗試獲得一個方法簽名。如果獲取不到,則直接呼叫doesNotRecognizeSelector
丟擲異常。如果能獲取,則返回非nil:建立一個 NSlnvocation 並傳給forwardInvocation:
。 -
呼叫
forwardInvocation:
方法,將第3步獲取到的方法簽名包裝成 Invocation 傳入,如何處理就在這裡面了,並返回非ni。 -
呼叫
doesNotRecognizeSelector:
,預設的實現是丟擲異常。如果第3步沒能獲得一個方法簽名,執行該步驟。
上面前4個方法均是模板方法,開發者可以override,由 runtime 來呼叫。最常見的實現訊息轉發:就是重寫方法3和4,吞掉一個訊息或者代理給其他物件都是沒問題的
也就是說_objc_msgForward
在進行訊息轉發的過程中會涉及以下這幾個方法:
-
resolveInstanceMethod:
方法 (或resolveClassMethod:
)。 -
forwardingTargetForSelector:
方法 -
methodSignatureForSelector:
方法 -
forwardInvocation:
方法 -
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
的前後差別:
有哪些場景需要直接呼叫_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_initWeak
和objc_destroyWeak
做下解釋:
總體說來,作用是: 通過objc_initWeak
函式初始化“附有weak修飾符的變數(obj1)”,在變數作用域結束時通過objc_destoryWeak
函式釋放該變數(obj1)。
下面分別介紹下方法的內部實現:
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);
前面的原始碼與下列原始碼相同。
// 使用虛擬碼模擬: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 和執行緒的關係: