Runtime 1 簡介,對象、類的結構,消息傳遞
前言
對于 runtime,看了很多的文章, 開發過程中也零零碎碎的用到過,感覺都不甚全面。反反復復研究了幾次,決定寫下來。對于初學者,感覺 runtime 很高深,不敢涉足;對于經常使用 runtime 的人來說,覺得 runtime 就那么點東西,不足為奇。其實在我反復研究它的過程中,每次都有收獲,因為它的原理不難,但是涉及很多,它貫穿這門語言,卻刀鋒尖利。
- 簡介
- 對象、類的結構
- objc_object
- objc_class
- 消息傳遞(Messaging)
- objc_method
- objc_msgSend
- 動態方法解析和轉發
- 動態方法解析
- 快速消息轉發
- 標準消息轉發
- 消息轉發與多繼承
- 消息轉發與代理對象
- Method Swizzling
- class_replaceMethod
- method_setImplementation
- method_exchangeImplementations
- Method Swizzling 的應用
- Method Swizzling 注意事項
- isa swizzling
- 介紹
- 應用之KVO
- 注意
持續更新中...
一. runtime 簡介
Objective-C 擴展了 C 語言,并加入了面向對象特性和 Smalltalk 式的消息傳遞機制。而這個擴展的核心是一個用 C 和 編譯語言 寫的 Runtime 庫。它是 Objective-C 面向對象和動態機制的基石。
Objective-C 是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個運行時系統來動態得創建類和對象、進行消息傳遞和轉發。
OC與C語言的區別:
- 對于C語言,函數的調用在編譯的時候會決定調用哪個函數。
- 對于OC的函數,屬于動態調用過程,在編譯的時候并不能決定真正調用哪個函數,只有在真正運行的時候才會根據函數的名稱找到對應的函數來調用。
- 在編譯階段,OC可以調用任何函數,即使這個函數并未實現,只要聲明過就不會報錯。
- 在編譯階段,C語言調用未實現的函數就會報錯。
二:對象、類的結構
在了解OC的消息傳遞之前,我們先明確對象、類的結構。
在OC中,類、對象都是一個C的結構體,從objc/objc.h
和objc/runtime.h
頭文件中,我們可以找到它們的定義:
typedef struct objc_class *Class;
typedef struct objc_object *id;
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; //指向它的類對象
};
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; //isa指針指向Meta Class,因為Objc的類的本身也是一個Object,為了處理這個關系,runtime就創造了Meta Class,當給類發送[NSObject alloc]這樣消息時,實際上是把這個消息發給了Class Object
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE; //父類
const char * _Nonnull name OBJC2_UNAVAILABLE; //類名
long version OBJC2_UNAVAILABLE; //類的版本信息,默認為0
long info OBJC2_UNAVAILABLE; //類信息,供運行期使用的一些位標識
long instance_size OBJC2_UNAVAILABLE; //該類的實例變量大小
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; //該類的成員變量鏈表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; //方法定義的鏈表
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; //方法緩存,對象接到一個消息會根據isa指針查找消息對象,這時會在method Lists中遍歷,如果cache了,常用的方法調用時就能夠提高調用的效率。
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; //協議鏈表
#endif
} OBJC2_UNAVAILABLE;
可以看到有__OBJC2__
和OBJC2_UNAVAILABLE
標識,在objc2中已經不可用了,但在runtime的源碼中,包含的內容大致是一樣的,只是結構略有不同。為了簡單,我們就以這個定義來理解類和對象的結構是沒問題的。
對象(objc_object)由isa指針和成員變量組成,其中isa指針指向它的類,其中成員變量包括所有父類和自己的成員變量:
Objective-C 對象的結構圖 |
---|
isa指針 |
根類的實例變量 |
倒數第二層父類的實例變量 |
... |
父類的實例變量 |
類的實例變量 |
例如:
@interface Father: NSObject{
int _father;
}
@end
@implementation Father
@end
@interface Child: Father {
int _child;
}
@end
@implementation Child
@end
int main(int argc, char * argv[]) {
Child *child = [[Child alloc] init];
return 0;
}
然后在return 0
的一行打斷點,運行程序在斷點停止時,在控制臺輸入p *child
,可以看到如下輸出:
(lldb) p *child
(Child) $0 = {
Father = (_father = 0)
_child = 0
}
這就是child對象的結構,從下到上分別是自己到根類的實例變量。
類(objc_class)主要組成:isa指向元類(Meta Class),super_class
指向父類、objc_method_list
存儲實例方法。類里面和對象一樣也有isa指針,說明類也是個對象,類是元類的實例。
元類(objc_class),在類對象里的isa指針也指向一個objc_class
類型的結構體,就是元類對象,結構和類對象一樣,但是objc_method_list
存儲的是類方法。
三、消息傳遞(Messaging)
I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is “messaging” – that is what the kernal[sic] of Smalltalk is all about... The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.
Alan Kay 曾多次強調 Smalltalk 的核心不是面向對象,面向對象只是 the lesser ideas,消息傳遞才是 the big idea。
在很多語言,比如 C ,調用一個方法其實就是跳到內存中的某一點并開始執行一段代碼。沒有任何動態的特性,因為這在編譯時就決定好了。而在 Objective-C 中,[object foo] 語法并不會立即執行 foo 這個方法的代碼。它是在運行時給 object 發送一條叫 foo 的消息。這個消息,也許會由 object 來處理,也許會被轉發給另一個對象,或者不予理睬假裝沒收到這個消息。多條不同的消息也可以對應同一個方法實現。這些都是在程序運行的時候決定的。
事實上,在編譯時你寫的 Objective-C 函數調用的語法都會被翻譯成一個 C 的函數調用-objc_msgSend
。比如,下面兩行代碼就是等價的:
[array insertObject:foo atIndex:5];
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
方法的結構(objc_method)
objc_class里的
objc_method_list本質是一個有
objc_method元素的可變長度的數組。
objc_method`的定義如下:
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
- SEL: 又叫選擇器,表示一個方法的名字。Objective-C在編譯時,會依據每一個方法的名字、參數序列,生成一個唯一的整型標識(Int類型的地址),這個標識就是SEL。
- IMP: 實際上是一個函數指針,指向方法實現的首地址。定義:
id (*IMP)(id, SEL, ...)
- method_types:表示函數參數及返回值類型的字符串 (見Type Encoding)
objc_msgSend
消息傳遞的關鍵在于 objc_object
中的 isa 指針和 objc_class
中的 class dispatch table。舉objc_msgSend(obj, foo)
這個例子來說:
- 首先,通過 obj 的 isa 指針找到它的 class ;
- 在 class 的 method list 找 foo ;
- 如果 class 中沒到 foo,繼續往它的 superclass 中找 ;
- 旦找到 foo 這個函數,就去執行它的實現IMP .
但這種實現有個問題,效率低。但一個 class 往往只有 20% 的函數會被經常調用,可能占總調用次數的 80% 。每個消息都需要遍歷一次objc_method_list
并不合理。如果把經常被調用的函數緩存下來,那可以大大提高函數查詢的效率。這也就是objc_class
中另一個重要成員objc_cache
做的事情 —— 再找到 foo 之后,把 foo 的method_name
作為 key ,method_imp
作為 value 給存起來。當再次收到 foo 消息的時候,可以直接在 cache 里找到,避免去遍歷objc_method_list
.
隱藏參數
當objc_msgSend
找到函數的實現,就會調用函數,并傳遞消息中所有的參數。也傳遞兩個隱藏參數到函數中:
- 接收對象
- 方法選擇器
這兩個參數為方法的實現提供了調用者的信息。之所以說是隱藏的,是因為它們在定義方法的源代碼中沒有聲明。它們是在編譯期被插入實現代碼的。
雖然這些參數沒有顯示聲明,但在代碼中仍然可以引用它們。我們可以使用self來引用接收者對象,使用_cmd來引用選擇器。
避免動態綁定
runtime的動態綁定讓我們寫代碼時更具有靈活性,可以在消息的傳遞過程中做一些處理,比如轉發或者交換方法的實現。不過靈活性也帶來了性能上的損耗,畢竟我們需要去查找方法的實現,而不像函數調用來得那么直接。當然,方法的緩存一定程度上解決了這一問題。
如果想要避開這種動態綁定方式,我們可以獲取方法實現的地址,然后像調用函數一樣來直接調用它。特別是當我們需要在一個循環內頻繁地調用一個特定的方法時,通過這種方式可以提高程序的性能。
NSObject類提供了methodForSelector:方法,讓我們可以獲取到方法的指針,然后通過這個指針來調用實現代碼:
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for (i = 0 ; i < 1000 ; i++)
setter(targetList[i], @selector(setFilled:), YES);
前兩個參數傳遞給接收對象(self)的程序和方法選擇器(_cmd)。這些參數在方法語法中是隱藏的,但當該方法當成函數調用時,必須是顯式的。