iOS必備技能之Runtime(一)
Runtime 是一個比較底層的C語言的API,可以翻譯為“運行時”。作為使用運行時機制的OC語言的底層,它在程序運行時把OC語言轉換成了runtime的C語言代碼。學習并理解runtime是OC學習歷程中的不可或缺的一大塊兒。
一、消息機制
調用方法的本質就是發送消息。
發送消息常見的有四個方法:
objc_msgSend
向一個類的實例發送消息,返回id類型數據。(這也是最常用的一個發送消息的方法)objc_msgSend_stret
向一個類的實例發送消息,返回結構體類型數據。objc_msgSendSuper
向一個類的實例的父類發送消息,返回id類型數據。objc_msgSendSuper_stret
向一個類的實例的父類發送消息,返回結構體類型的數據。
在OC語言中,方法的真正實現是在程序運行的時候綁定的,假如一個方法只有聲明,沒有實現,調用后在編譯階段是不會出錯的,真正報錯是在運行的時候。
[receiver message]
以上方法在運行時會被轉化為
//receiver是方法的調用者,selector是方法名
objc_msgSend(receiver, selector)
//如果有參數
objc_msgSend(receiver, selector, arg1, arg2, ...)
發送消息的原理
objc_msgSend為了完成動態綁定,進行了以下三步:
- 首先它要先根據方法名找到方法的具體實現程序,因為多態性,同一個方法在不同的類里面可以有不同的實現,所以查找主要依靠尋找receiver所在的類。
- 傳遞參數,調用該方法的實現程序。
- 把該程序的返回值作為方法自己的返回值。
//runtime中對類的定義
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
if !OBJC2
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
endif
} OBJC2_UNAVAILABLE;
//runtime中對實例的定義
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};</code></pre>
如上runtime中對類的定義,每一個類都有指向父類的指針(super_class
)和一個方法調度表(objc_method_list **methodLists
:根據方法名SEL查找該方法的具體實現的地址IMP),當向一個對象發送消息的時候,該對象通過isa指針找到該對象的類(實際上,實例的定義里面也只有這個指針,沒有別的了),在類的調度表查找該方法名,當找不到的時候,通過指向父類的指針找到該類的父類,然后在該類的父類中繼續查找該方法名,這樣遞歸查找一直到NSObject類為止(NSProxy類除外,它不屬于NSObject子類)。如果查找到該方法名,根據調度表找到該方法的實現的地址進行調用。如下圖所示

Messaging Framework
為了加速發送消息的進程,runtime系統會把使用過的方法名和對應的內存地址緩存起來,每個類都有一個單獨的緩存空間,其中包含自己類的方法和繼承自父類的方法。在查找調度表之前,runtime系統會首先在緩存中進行查找。
使用隱藏的參數
當objc_msgSend找到方法的實現程序時,它調用這個程序并傳遞所有方法的參數給它,這其中還包含兩個隱藏的參數:
- 消息的接收對象
- 調用方法的方法名(selector)
這兩個參數雖然沒有在方法中進行定義,但是你可以很方便地使用它們。消息的接收對象通過self來引用,方法名通過_cmd來引用。
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}</code></pre>
獲取方法的地址
避免動態綁定的唯一方法就是直接獲得方法的地址然后把它當做函數一樣來調用。當一個方法被連續多次執行,而你又不想每次都用消息機制造成額外的開支,這種辦法就是一個合適的使用時機。
下面的例子展示了如何節省開支多次調用setFilled:方法
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);</code></pre>
通過methodForSelector:
方法,你可以請求得到指向實現該方法的程序的指針,然后通過這個指針調用該程序。值的注意的是,參數和返回值要正確聲明,而且參數中id和SEL要進行顯式聲明。
二、動態方法
假如你想動態地為方法提供實現,OC使用@dynamic
實現了這個特性。
@dynamic propertyName;
這樣就會通知編譯器和這個屬性相關的方法將會動態提供。你可以通過方法resolveInstanceMethod:
和resolveClassMethod:
分別為類方法和實例方法動態地提供實現。
一個OC的方法其實就是由C語言的函數再加上至少兩個參數(self和_cmd)組成的。
你可以把一個函數通過class_addMethod
作為方法添加到一個類中去。給定以下一個函數:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
你可以通過resolveInstanceMethod:
這個方法把上面的函數以方法名(resolveThisMethodDynamically
)動態地添加到一個類(MyClass)里面。具體實現方式如下:
@implementation MyClass
- (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end</code></pre> 這其中,class_addMethod
這個方法有四個參數,第一個是要添加方法的類,第二個是要添加的方法名,第三個是這個方法的實現函數的指針(值的注意的是,這個函數必須顯式地把self
和_cmd
這兩個參數寫出來),第四個是方法的參數數組,在這里它是用的類型編碼的方式進行表示的,因為方法一定含有self
和_cmd
這兩個參數,所以字符數組的第二個和第三個字符一定是"@:",第一個字符代表返回值,這里為空用“v”來表示。相關知識點請見下文。
三、類型編碼
為了使runtime系統更加簡潔,編譯器把每個方法的返回值和參數的類型都分別使用一個字符來編碼,然后再把它們關聯到方法選擇器(selector)上。因為這種編碼方案在其它環境中也很實用,所以我們可以很方便地使用@encode()
編譯器指令來自定義類似的編碼。
char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);
一般來說,不管是基本類型,還是指針,或者結構體,或者聯合體,甚至可以是類名,只要這個類型能夠作為C語言中sizeof()
的參數,那么它就能被進行編碼。
下表便是已經定義了的類型編碼,使用@encode()
編譯器指令自定義編碼的時候一定要避開這些字符。


Objective-C type encodings
特別注意,OC不支持long double
類型,因此@encode(long double)
會返回字符“d",意義為double
。
結構體的類型編碼是按照結構體內部的類型的順序來表示的,比如
typedef struct example {
id anObject;
char *aString;
int anInt;
} Example;
會被編碼為:
{example=@*i}
由第一章內容可以得知,類的實例的定義是一個只包含isa指針的結構體,所以[NSObject class]
會被編碼為
{NSObject=#}
具體應用方面,上一章class_addMethod
最后一個參數就是使用的類型編碼來表示的函數返回值和參數的類型。
文/陌淺Ivan(簡書)