前言
這篇文章講解了一些runtime
的基本知識,需要有一定的objc
基礎及開發經驗。為了更好的閱讀體驗,推薦戳我的博客閱讀。
從理解發送消息講起
為什么objc
叫消息發送?為什么不叫函數調用?在objc
中,發送消息僅僅表示一種行為 ,不能理解為像C
語言中那樣的函數調用。原因就是在發送消息的背后,runtime
幫我們做了非常多的事情。這樣是objc
能真正成為一門動態語言的真正原因。
要學習runtime
所要掌握的幾個基本概念
在開始學習runtime
之前,有幾個基本的概念是必須要了解的:
SEL
SEL
是selector
在objc
中的表示類型,selector
是方法選擇器,可以理解為方法的ID。而這個ID的數據結構是SEL
:
typedef struct objc_selector *SEL
其實它就是個映射到方法的C字符串,你可以用 Objc
編譯器命令@selector()
或者 Runtime
系統的sel_registerName
函數來獲得一個SEL
類型的方法選擇器。
id
objc_msgSend
第一個參數類型為id
,大家對它都不陌生,它是一個指向類實例的指針:
typedef struct objc_object *id
那么objc_object
又是啥呢:
struct objc_object {
Class isa;
};
objc_object
結構體包含一個isa
指針,根據isa
指針就可以順藤摸瓜找到對象所屬的類。
Class
之所以說isa
是指針是因為Class
其實是一個指向objc_class
結構體的指針:
typedef struct objc_class *Class;
objc_class
定義如下:
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;
-
類對象的概念在此不再表述,只說一些之前自己不懂的部分,你也可以查看我的這篇深入理解objc中的對象與類博客:
- 類對象的
isa
指針指向的是類對象的元類,每個類對象都有自己的元類。 - 類對象里存放時的實例對象的對象方法,屬性,協議列表等信息。注意
objc_cache *cache
這個東西,存的是匹配信息,比如消息來到時該對象能不能處理此消息,方法對應的實現等都收納在此中。 - 只有類對象才有
super_class
這個指針。 -
NSObject
的super_class
指針指向nil
。
- 類對象的
-
關于元類的概念:
- 元類是類對象的類對象,類對象的對象方法(即實例對象的類方法)列表就存在此處。
- 所有元類的
isa
指針指向NSObject
的元類,即根元類。super_class
指針指向NSObject
。
如下圖:
Method
Method是一種代表類中的某個方法的類型。
typedef struct objc_method *Method;
objc_method
:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
- 方法名類型為
SEL
,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。 - 方法類型
method_types
是個char
指針,其實存儲著方法的參數類型和返回值類型。 -
method_imp
指向了方法的實現,本質上是一個函數指針,后面會詳細講到。
IMP
IMP在objc.h中的定義是:
typedef id (*IMP)(id, SEL, ...);
它就是一個函數指針,這是由編譯器生成的。當你發起一個
ObjC
消息之后,最終它會執行的那段代碼,就是由這個函數指針指定的。而IMP
這個函數指針就指向了這個方法的實現。既然得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法,這在后面會提到。你會發現
IMP
指向的方法與objc_msgSend
函數類型相同,參數都包含id
和SEL
類型。每個方法名都對應一個SEL
類型的方法選擇器,而每個實例對象中的SEL對應的方法實現肯定是唯一的,通過一組id
和SEL
參數就能確定唯一的方法實現地址;反之亦然。
runtime
做的那些事
首先需要明確的是,在objc
中,直到運行時才將消息與方法實現綁定。而這些工作都是runtime
為我們做的。
runtime
之消息轉發
其實[receiver message]
會被編譯器轉化為:
objc_msgSend(receiver, selector)
如果消息含有參數,則為:
objc_msgSend(receiver, selector, arg1, arg2, ...)
在平時的調用中,看起來像是objc_msgSend
返回了數據,其實objc_msgSend
從不返回數據而是你的方法被調用后返回了數據。下面詳細敘述下消息發送步驟:
- 檢測這個
selector
是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會retain
,release
這些函數了。 - 檢測這個
target
是不是 nil 對象。ObjC
的特性是允許對一個nil
對象執行任何一個方法不會 Crash,因為會被忽略掉。 - 如果上面兩個都過了,那就開始查找這個類的
IMP
,先從cache
里面找,完了找得到就跳到對應的函數去執行。 - 如果 cache 找不到就找一下方法分發表(即
class
里的method_list
表)。 - 如果分發表找不到就到超類的分發表去找,一直找,直到找到
NSObject
類為止。 - 如果還找不到就要開始進入動態方法解析了,后面會提到。
當然,objc_msgSend
函數只是一般情況下的調用,還有會有例如給父類發送消息,返回值是結構體而不是數值等情況,會采用其他的例如objc_msgSendSuper
函數等,在此不再敘述。
消息轉發的第一步: 動態方法解析
所謂動態方法解析是發生在objc_msgSend
函數查找完所有類對象or元類方法列表后仍未找到方法實現第一個調用的方法,動態方法解析發生在消息轉發之前:
+ (BOOL)resolveClassMethod:(SEL)sel __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);//找不到相應的類方法調用
+ (BOOL)resolveInstanceMethod:(SEL)sel __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);//找不到相應的對象方法調用
在平時的開發中,最常用的情況就是聲明某個屬性為@dynamic
后,需要我們自己提供setter
及getter
方法時。如:
@dynamic propertyName;
添加如上關鍵字修飾屬性表示告訴編譯器我們會動態的提供存取方法,此時動態方法解析就是個不錯的選擇:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@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
第二步: 重定向
動態方法解析雖然強大,可以動態地為一個類添加方法,但它也有個很大的弊端:只能為當前類添加方法。如果我們想在消息無法解讀時調用其他類的方法呢?此時就要用到重定向了。
重定向是發生在動態方法解析之后,完整的消息轉發之后的。在此處我們就可以動態的將一條消息轉換為其他類的調用:
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(intstanceNoImpMethod)) {
return [testClass class];
}
return [super forwardingTargetForSelector:aSelector];
}
需要注意的時,此時我們將消息轉發給了testClass
這個類,就表示我們希望這個不能被當前類解讀的消息中的sel
能被testClass
執行。如果testClass
類中沒有這個sel
的類方法,程序一樣會crash:
+[testClass intstanceNoImpMethod]: unrecognized selector sent to class 0x103122ea8
如果我們在此方法中直接返回self
或者nil
,都會跳過這個步驟直接進入完整的消息轉發機制。
所以如果你想把這個消息解讀為其他類的對象方法,就要返回這個類的對象,如果想解讀為類方法,就要返回類對象。
同理,如果你想轉發一個含有類方法的消息,就應該調用:
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(intstanceNoImpMethod)) {
return [testClass new];
}
return [super forwardingTargetForSelector:aSelector];
}
于是你可以大開腦洞:把一個類的類方法or對象方法換成另一個類的對象方法or類方法,怎么組合隨你,只要注意sel
的名稱一致即可。有木有感覺很酷??
ps:我在此處混用了類與類對象,其實這倆是一個東西,如果你不能理解,戳這里:深入理解objc中的對象與類
可見,利用重定向是在objc
模擬多繼承的一種方法。但是重定向和動態方法解析一樣都有弊端,那就是還是不夠“靈活”。消息中sel
不能被我們更換。設想這種情景:如果我們又想更換sel
,又想更換消息的接受者,此時我們該怎么做?
第三步:完整的消息轉發機制
上文我們降到了如果動態方法解析失敗,進入重定向,那么重定向也失敗了,就來到了完整的消息轉發機制:
這里遇到了些小困難,接下來在填坑吧
runtime
之健壯的實例變量
@property
大家每天都在用,但也許你不知道,runtime
在你為類添加了一個屬性時,它會將這個成員變量(iva
)存放在類對象里。這和比如JAVA
等語言有著很大的不同:
objc
將實例變量當做一種存儲偏移量(offset)所用的特殊變量交由類對象保管。偏移量會運行期間查找,如果類的定義變了,那存儲的偏移量也就變了。這樣的話,無論何時訪問實例變量,都能獲取到正確的值。
基于這個原因,我們才能該動態的給一個類添加屬性,因為屬性列表本來就是動態查找的。
8月21日更新:關于健壯的實例變量,還有這樣的說法:
當一個類定義了某些成員變量后編譯一次后,再次改變該類的成員變量,會導致偏移量發生改變。在有
runtime
的情況下,它會自動幫你調整偏移量,以保證不用再次編譯文件。
關聯對象
關聯對象指的是動態的為一個對象添加變量,之前有寫過介紹的短文:在分類中給類添加屬性。
神奇的Method Swizzling
之前所說的消息轉發雖然功能強大,但需要我們了解并且能更改對應類的源代碼,因為我們需要實現自己的轉發邏輯。當我們無法觸碰到某個類的源代碼,卻想更改這個類某個方法的實現時,該怎么辦呢?可能繼承類并重寫方法是一種想法,但是有時無法達到目的。這里介紹的是 Method Swizzling ,它通過重新映射方法對應的實現來達到“偷天換日”的目的。跟消息轉發相比,Method Swizzling 的做法更為隱蔽,甚至有些冒險,也增大了debug的難度。
上一個??:
+ (void)load{
Class aClass = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(cumtomMethod:);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
調用:
- (void)cumtomMethod:(BOOL)animted{
[self cumtomMethod:animted];
NSLog(@"1111");
}
輸出:
2016-08-18 00:39:04.658 總結測試[80016:3763455] 1111
我剛開始看這段代碼也是暈的一塌糊涂,現在回想起來是沒能立即SEL
與IMP
,下面是具體的調用過程:
系統調用viewWillAppear:
(SEL
) ----> 來到了customMethod
的IMP
-----> 我們自己調用customMethod:
的SEL
-----> 系統viewWillAppear:
的IMP
目前為止小弟也只是知道有這么個東西,還真沒用到過這玩意。真有興趣可以看看這篇:Objective-C的hook方案(一): Method Swizzling
8-21凌晨補充下:最近基友分享了一篇關于Method Swizzling
的應用方案,是騰訊一面提到的。有興趣可以看下。
扯扯淡??
這篇文章花了不少心血,也通過撰寫這篇文章徹底重新認識了Runtime
這個之前小白時看都不敢看的東西。也觀摩了不少大神的博客,感覺平時應該多注意這些好的資源,有時候比悶頭寫代碼強不少??。
在此放一下我特別喜歡的一位博主的博客:玉令天下的博客,就像引言所說,這篇文章不過是我讀這為大神博客的學習筆記罷了。作為同齡的開發者,很是汗顏啊,共勉吧~~