1. 程式人生 > >Objective-C 訊息傳送與轉發機制原理

Objective-C 訊息傳送與轉發機制原理

原文地址:http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/#原始碼解析

本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具備了靈活的動態特性,使這門古老的語言煥發生機。主要內容如下:

  • 引言
  • 簡介
  • 與Runtime互動
  • Runtime術語
  • 訊息
  • 動態方法解析
  • 訊息轉發
  • 健壯的例項變數(Non Fragile ivars)
  • Objective-C Associated Objects
  • Method Swizzling
  • 總結

引言

曾經覺得Objc特別方便上手,面對著 Cocoa 中大量 API,只知道簡單的查文件和呼叫。還記得初學 Objective-C 時把[receiver message]

當成簡單的方法呼叫,而無視了“傳送訊息”這句話的深刻含義。其實[receiver message]會被編譯器轉化為:

1
objc_msgSend(receiver, selector)

如果訊息含有引數,則為:

1
objc_msgSend(receiver, selector, arg1, arg2, ...)

如果訊息的接收者能夠找到對應的selector,那麼就相當於直接執行了接收者這個物件的特定方法;否則,訊息要麼被轉發,或是臨時向接收者動態新增這個selector對應的實現內容,要麼就乾脆玩完崩潰掉。

現在可以看出[receiver message]真的不是一個簡簡單單的方法呼叫。因為這只是在編譯階段確定了要向接收者傳送message

這條訊息,而receive將要如何響應這條訊息,那就要看執行時發生的情況來決定了。

Objective-C 的 Runtime 鑄就了它動態語言的特性,這些深層次的知識雖然平時寫程式碼用的少一些,但是卻是每個 Objc 程式設計師需要了解的。

簡介

因為Objc是一門動態語言,所以它總是想辦法把一些決定工作從編譯連線推遲到執行時。也就是說只有編譯器是不夠的,還需要一個執行時系統 (runtime system) 來執行編譯後的程式碼。這就是 Objective-C Runtime 系統存在的意義,它是整個Objc執行框架的一塊基石。

Runtime其實有兩個版本:“modern”和 “legacy”。我們現在用的 Objective-C 2.0 採用的是現行(Modern)版的Runtime系統,只能執行在 iOS 和 OS X 10.5 之後的64位程式中。而OS X較老的32位程式仍採用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系統。這兩個版本最大的區別在於當你更改一個類的例項變數的佈局時,在早期版本中你需要重新編譯它的子類,而現行版就不需要。

Runtime基本是用C和彙編寫的,可見蘋果為了動態系統的高效而作出的努力。你可以在這裡下到蘋果維護的開原始碼。蘋果和GNU各自維護一個開源的runtime版本,這兩個版本之間都在努力的保持一致。

與Runtime互動

Objc 從三種不同的層級上與 Runtime 系統進行互動,分別是通過 Objective-C 原始碼,通過 Foundation 框架的NSObject類定義的方法,通過對 runtime 函式的直接呼叫。

Objective-C原始碼

大部分情況下你就只管寫你的Objc程式碼就行,runtime 系統自動在幕後辛勤勞作著。
還記得引言中舉的例子吧,訊息的執行會使用到一些編譯器為實現動態語言特性而建立的資料結構和函式,Objc中的類、方法和協議等在 runtime 中都由一些資料結構來定義,這些內容在後面會講到。(比如objc_msgSend函式及其引數列表中的idSEL都是啥)

NSObject的方法

Cocoa 中大多數類都繼承於NSObject類,也就自然繼承了它的方法。最特殊的例外是NSProxy,它是個抽象超類,它實現了一些訊息轉發有關的方法,可以通過繼承它來實現一個其他類的替身類或是虛擬出一個不存在的類,說白了就是領導把自己展現給大家風光無限,但是把活兒都交給幕後小弟去幹。

有的NSObject中的方法起到了抽象介面的作用,比如description方法需要你過載它併為你定義的類提供描述內容。NSObject還有些方法能在執行時獲得類的資訊,並檢查一些特性,比如class返回物件的類;isKindOfClass:isMemberOfClass:則檢查物件是否在指定的類繼承體系中;respondsToSelector:檢查物件能否響應指定的訊息;conformsToProtocol:檢查物件是否實現了指定協議類的方法;methodForSelector:則返回指定方法實現的地址。

Runtime的函式

Runtime 系統是一個由一系列函式和資料結構組成,具有公共介面的動態共享庫。標頭檔案存放於/usr/include/objc目錄下。許多函式允許你用純C程式碼來重複實現 Objc 中同樣的功能。雖然有一些方法構成了NSObject類的基礎,但是你在寫 Objc 程式碼時一般不會直接用到這些函式的,除非是寫一些 Objc 與其他語言的橋接或是底層的debug工作。在Objective-C Runtime Reference中有對 Runtime 函式的詳細文件。

Runtime術語

還記得引言中的objc_msgSend:方法吧,它的真身是這樣的:

1
id objc_msgSend ( id self, SEL op, ... );

下面將會逐漸展開介紹一些術語,其實它們都對應著資料結構。

SEL

objc_msgSend函式第二個引數型別為SEL,它是selector在Objc中的表示型別(Swift中是Selector類)。selector是方法選擇器,可以理解為區分方法的 ID,而這個 ID 的資料結構是SEL:

1
typedef struct objc_selector *SEL;

其實它就是個對映到方法的C字串,你可以用 Objc 編譯器命令@selector()或者 Runtime 系統的sel_registerName函式來獲得一個SEL型別的方法選擇器。

不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變數型別不同也會導致它們具有相同的方法選擇器,於是 Objc 中方法命名有時會帶上引數型別(NSNumber一堆抽象工廠方法拿走不謝),Cocoa 中有好多長長的方法哦。

id

objc_msgSend第一個引數型別為id,大家對它都不陌生,它是一個指向類例項的指標:

1
typedef struct objc_object *id;

objc_object又是啥呢:

1
struct objc_object { Class isa; };

objc_object結構體包含一個isa指標,根據isa指標就可以順藤摸瓜找到物件所屬的類。

PS:isa指標不總是指向例項物件所屬的類,不能依靠它來確定型別,而是應該用class方法來確定例項物件的類。因為KVO的實現機理就是將被觀察物件的isa指標指向一箇中間類而不是真實的類,這是一種叫做 isa-swizzling 的技術,詳見官方文件

Class

之所以說isa是指標是因為Class其實是一個指向objc_class結構體的指標:

1
typedef struct objc_class *Class;

objc_class就是我們摸到的那個瓜,裡面的東西多著呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

可以看到執行時一個類還關聯了它的超類指標,類名,成員變數,方法,快取,還有附屬的協議。

PS:OBJC2_UNAVAILABLE之類的巨集定義是蘋果在 Objc 中對系統執行版本進行約束的黑魔法,為的是相容非Objective-C 2.0的遺留邏輯,但我們仍能從中獲得一些有價值的資訊,有興趣的可以檢視原始碼。

Objective-C 2.0 的標頭檔案雖然沒暴露出objc_class結構體更詳細的設計,我們依然可以從Objective-C 1.0 的定義中小窺端倪:

objc_class結構體中:ivarsobjc_ivar_list指標;methodLists是指向objc_method_list指標的指標。也就是說可以動態修改*methodLists的值來新增成員方法,這也是Category實現的原理,同樣解釋了Category不能新增屬性的原因。而最新版的 Runtime 原始碼對這一塊的描述已經有很大變化,可以參考下美團技術團隊的深入理解Objective-C:Category
PS:任性的話可以在Category中新增@dynamic的屬性,並利用執行期動態提供存取方法或乾脆動態轉發;或者乾脆使用關聯度物件(AssociatedObject)

其中objc_ivar_listobjc_method_list分別是成員變數列表和方法列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;

int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}

如果你C語言不是特別好,可以直接理解為objc_ivar_list結構體儲存著objc_ivar陣列列表,而objc_ivar結構體儲存了類的單個成員變數的資訊;同理objc_method_list結構體儲存著objc_method陣列列表,而objc_method結構體儲存了類的某個方法的資訊。

最後要提到的還有一個objc_cache,顧名思義它是快取,它在objc_class的作用很重要,在後面會講到。

不知道你是否注意到了objc_class中也有一個isa物件,這是因為一個 ObjC 類本身同時也是一個物件,為了處理類和物件的關係,runtime 庫建立了一種叫做元類 (Meta Class) 的東西,類物件所屬型別就叫做元類,它用來表述類物件本身所具備的元資料。類方法就定義於此處,因為這些方法可以理解成類物件的例項方法。每個類僅有一個類物件,而每個類物件僅有一個與之相關的元類。當你發出一個類似[NSObject alloc]的訊息時,你事實上是把這個訊息發給了一個類物件 (Class Object) ,這個類物件必須是一個元類的例項,而這個元類同時也是一個根元類 (root meta class) 的例項。所有的元類最終都指向根元類為其超類。所有的元類的方法列表都有能夠響應訊息的類方法。所以當 [NSObject alloc] 這條訊息發給類物件的時候,objc_msgSend()會去它的元類裡面去查詢能夠響應訊息的方法,如果找到了,然後對這個類物件執行方法呼叫。

上圖實線是 super_class 指標,虛線是isa指標。 有趣的是根元類的超類是NSObject,而isa指向了自己,而NSObject的超類為nil,也就是它沒有超類。

Method

Method是一種代表類中的某個方法的型別。

1
typedef struct objc_method *Method;

objc_method在上面的方法列表中提到過,它儲存了方法名,方法型別和方法實現:

1
2
3
4
5
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指向了方法的實現,本質上是一個函式指標,後面會詳細講到。

Ivar

Ivar是一種代表類中例項變數的型別。

1
typedef struct objc_ivar *Ivar;

objc_ivar在上面的成員變數列表中也提到過:

1
2
3
4
5
6
7
8
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

可以根據例項查詢其在類中的名字,也就是“反射”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {//此處若 crash 不要慌!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}

class_copyIvarList 函式獲取的不僅有例項變數,還有屬性。但會在原本的屬性名前加上一個下劃線。

IMP

IMPobjc.h中的定義是:

1
typedef id (*IMP)(id, SEL, ...);

它就是一個函式指標,這是由編譯器生成的。當你發起一個 ObjC 訊息之後,最終它會執行的那段程式碼,就是由這個函式指標指定的。而 IMP 這個函式指標就指向了這個方法的實現。既然得到了執行某個例項某個方法的入口,我們就可以繞開訊息傳遞階段,直接執行方法,這在後面會提到。

你會發現IMP指向的方法與objc_msgSend函式型別相同,引數都包含idSEL型別。每個方法名都對應一個SEL型別的方法選擇器,而每個例項物件中的SEL對應的方法實現肯定是唯一的,通過一組idSEL引數就能確定唯一的方法實現地址;反之亦然。

Cache

runtime.h中Cache的定義如下:

1
typedef struct objc_cache *Cache

還記得之前objc_class結構體中有一個struct objc_cache *cache吧,它到底是快取啥的呢,先看看objc_cache的實現:

1
2
3
4
5
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};

Cache為方法呼叫的效能進行優化,通俗地講,每當例項物件接收到一個訊息時,它不會直接在isa指向的類的方法列表中遍歷查詢能夠響應訊息的方法,因為這樣效率太低了,而是優先在Cache中查詢。Runtime 系統會把被呼叫的方法存到Cache中(理論上講一個方法如果被呼叫,那麼它有可能今後還會被呼叫),下次查詢的時候效率更高。這根計算機組成原理中學過的 CPU 繞過主存先訪問Cache的道理挺像,而我猜蘋果為提高Cache命中率應該也做了努力吧。

Property

@property標記了類中的屬性,這個不必多說大家都很熟悉,它是一個指向objc_property結構體的指標:

1
2
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個更常用

可以通過class_copyPropertyList 和 protocol_copyPropertyList方法來獲取類和協議中的屬性:

1
2
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

返回型別為指向指標的指標,哈哈,因為屬性列表是個陣列,每個元素內容都是一個objc_property_t指標,而這兩個函式返回的值是指向這個陣列的指標。

舉個栗子,先宣告一個類:

1
2
3
4
5
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end

你可以用下面的程式碼獲取屬性列表:

1
2
3
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

你可以用property_getName函式來查詢屬性名稱:

1
const char *property_getName(objc_property_t property)

你可以用class_getProperty 和 protocol_getProperty通過給出的名稱來在類和協議中獲取屬性的引用:

1
2
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

你可以用property_getAttributes函式來發掘屬性的名稱和@encode型別字串:

1
const char *property_getAttributes(objc_property_t property)

把上面的程式碼放一起,你就能從一個類中獲取它的屬性啦:

1
2
3
4
5
6
7
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property= properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}

對比下 class_copyIvarList 函式,使用 class_copyPropertyList 函式只能獲取類的屬性,而不包含成員變數。但此時獲取的屬性名是不帶下劃線的。

訊息

前面做了這麼多鋪墊,現在終於說到了訊息了。Objc 中傳送訊息是用中括號([])把接收者和訊息括起來,而直到執行時才會把訊息與方法實現繫結。

有關訊息傳送和訊息轉發機制的原理,可以檢視這篇文章

objc_msgSend函式

在引言中已經對objc_msgSend進行了一點介紹,看起來像是objc_msgSend返回了資料,其實objc_msgSend從不返回資料而是你的方法被呼叫後返回了資料。下面詳細敘述下訊息傳送步驟:

  1. 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retainrelease 這些函數了。
  2. 檢測這個 target 是不是 nil 物件。ObjC 的特性是允許對一個 nil 物件執行任何一個方法不會 Crash,因為會被忽略掉。
  3. 如果上面兩個都過了,那就開始查詢這個類的 IMP,先從 cache 裡面找,完了找得到就跳到對應的函式去執行。
  4. 如果 cache 找不到就找一下方法分發表。
  5. 如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止。
  6. 如果還找不到就要開始進入動態方法解析了,後面會提到。

PS:這裡說的分發表其實就是Class中的方法列表,它將方法選擇器和方法實現地址聯絡起來。

其實編譯器會根據情況在objc_msgSendobjc_msgSend_stretobjc_msgSendSuper, 或 objc_msgSendSuper_stret四個方法中選擇一個來呼叫。如果訊息是傳遞給超類,那麼會呼叫名字帶有”Super”的函式;如果訊息返回值是資料結構而不是簡單值時,那麼會呼叫名字帶有”stret”的函式。排列組合正好四個方法。

值得一提的是在 i386 平臺處理返回型別為浮點數的訊息時,需要用到objc_msgSend_fpret函式來進行處理,這是因為返回型別為浮點數的函式對應的 ABI(Application Binary Interface) 與返回整型的函式的 ABI 不相容。此時objc_msgSend不再適用,於是objc_msgSend_fpret被派上用場,它會對浮點數暫存器做特殊處理。不過在 PPC 或 PPC64 平臺是不需要麻煩它的。

PS:有木有發現這些函式的命名規律哦?帶“Super”的是訊息傳遞給超類;“stret”可分為“st”+“ret”兩部分,分別代表“struct”和“return”;“fpret”就是“fp”+“ret”,分別代表“floating-point”和“return”。

方法中的隱藏引數

我們經常在方法中使用self關鍵字來引用例項本身,但從沒有想過為什麼self就能取到呼叫當前方法的物件吧。其實self的內容是在方法執行時被偷偷的動態傳入的。

objc_msgSend找到方法對應的實現時,它將直接呼叫該方法實現,並將訊息中所有的引數都傳遞給方法實現,同時,它還將傳遞兩個隱藏的引數:

  • 接收訊息的物件(也就是self指向的內容)
  • 方法選擇器(_cmd指向的內容)

之所以說它們是隱藏的是因為在原始碼方法的定義中並沒有宣告這兩個引數。它們是在程式碼被編譯時被插入實現中的。儘管這些引數沒有被明確宣告,在原始碼中我們仍然可以引用它們。在下面的例子中,self引用了接收者物件,而_cmd引用了方法本身的選擇器:

1
2
3
4
5
6
7
8
9
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();

if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}

在這兩個引數中,self 更有用。實際上,它是在方法實現中訪問訊息接收者物件的例項變數的途徑。

而當方法中的super關鍵字接收到訊息時,編譯器會建立一個objc_super結構體:

1
struct objc_super { id receiver; Class class; };

這個結構體指明瞭訊息應該被傳遞給特定超類的定義。但receiver仍然是self本身,這點需要注意,因為當我們想通過[super class]獲取超類時,編譯器只是將指向selfid指標和class的SEL傳遞給了objc_msgSendSuper函式,因為只有在NSObject類才能找到class方法,然後class方法呼叫object_getClass(),接著呼叫objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個引數是指向selfid指標,與呼叫[self class]相同,所以我們得到的永遠都是self的型別。

獲取方法地址

IMP那節提到過可以避開訊息繫結而直接獲取方法的地址並呼叫方法。這種做法很少用,除非是需要持續大量重複呼叫某方法的極端情況,避開訊息傳送氾濫而直接呼叫該方法會更高效。

NSObject類中有個methodForSelector:例項方法,你可以用它來獲取某個方法選擇器對應的IMP,舉個栗子:

1
2
3
4
5
6
7
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);

當方法被當做函式呼叫時,上節提到的兩個隱藏引數就需要我們明確給出了。上面的例子呼叫了1000次函式,你可以試試直接給target傳送1000次setFilled:訊息會花多久。

PS:methodForSelector:方法是由 Cocoa 的 Runtime 系統提供的,而不是 Objc 自身的特性。

動態方法解析

你可以動態地提供一個方法的實現。例如我們可以用@dynamic關鍵字在類的實現檔案中修飾一個屬性:

1
@dynamic propertyName;

這表明我們會為這個屬性動態提供存取方法,也就是說編譯器不會再預設為我們生成setPropertyName:propertyName方法,而需要我們動態提供。我們可以通過分別過載resolveInstanceMethod:resolveClassMethod:方法分別新增例項方法實現和類方法實現。因為當 Runtime 系統在Cache和方法分發表中(包括超類)找不到要執行的方法時,Runtime會呼叫resolveInstanceMethod:resolveClassMethod:來給程式設計師一次動態新增方法實現的機會。我們需要用class_addMethod函式完成向特定類新增特定方法實現的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "[email protected]:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子為resolveThisMethodDynamically方法添加了實現內容,也就是dynamicMethodIMP方法中的程式碼。其中 “[email protected]:” 表示返回值和引數,這個符號涉及 Type Encoding

PS:動態方法解析會在訊息轉發機制浸入前執行。如果 respondsToSelector: 或 instancesRespondToSelector:方法被執行,動態方法解析器將會被首先給予一個提供該方法選擇器對應的IMP的機會。如果你想讓該方法選擇器被傳送到轉發機制,那麼就讓resolveInstanceMethod:返回NO

評論區有人問如何用 resolveClassMethod: 解析類方法,我將他貼出有問題的程式碼做了糾正和優化後如下,可以順便將例項方法和類方法的動態方法解析對比下:
標頭檔案:

1
2
3
4
5
6
#import <Foundation/Foundation.h>

@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end

m 檔案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#import "Student.h"
#import <objc/runtime.h>

@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(learnClass:)) {
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "[email protected]:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(goToSchool:)) {
class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "[email protected]:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}

+ (void)myClassMethod:(NSString *)string {
NSLog(@"myClassMethod = %@", string);
}

- (void)myInstanceMethod:(NSString *)string {
NSLog(@"myInstanceMethod = %@", string);
}
@end

需要深刻理解 [self class] 與 object_getClass(self) 甚至 object_getClass([self class]) 的關係,其實並不難,重點在於 self 的型別:

  1. 當 self 為例項物件時,[self class] 與 object_getClass(self) 等價,因為前者會呼叫後者。object_getClass([self class]) 得到元類。
  2. 當 self 為類物件時,[self class] 返回值為自身,還是 selfobject_getClass(self) 與object_getClass([self class]) 等價。

凡是涉及到類方法時,一定要弄清楚元類、selector、IMP 等概念,這樣才能做到舉一反三,隨機應變。

訊息轉發

重定向

在訊息轉發機制執行前,Runtime 系統會再給我們一次偷樑換柱的機會,即通過過載- (id)forwardingTargetForSelector:(SEL)aSelector方法替換訊息的接受者為其他物件:

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}

畢竟訊息轉發要耗費更多時間,抓住這次機會將訊息重定向給別人是個不錯的選擇,不過千萬別返回self,因為那樣會死迴圈。 如果此方法返回nil或self,則會進入訊息轉發機制(forwardInvocation:);否則將向返回的物件重新發送訊息。

轉發

當動態方法解析不作處理返回NO時,訊息轉發機制會被觸發。在這時forwardInvocation:方法會被執行,我們可以重寫這個方法來定義我們的轉發邏輯:

1
2
3
4
5
6
7
8
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}

該訊息的唯一引數是個NSInvocation型別的物件——該物件封裝了原始的訊息和訊息的引數。我們可以實現forwardInvocation:方法來對不能處理的訊息做一些預設的處理,也可以將訊息轉發給其他物件來處理,而不丟擲錯誤。

這裡需要注意的是引數anInvocation是從哪的來的呢?其實在forwardInvocation:訊息傳送前,Runtime系統會向物件傳送methodSignatureForSelector:訊息,並取到返回的方法簽名用於生成NSInvocation物件。所以我們在重寫forwardInvocation:的同時也要重寫methodSignatureForSelector:方法,否則會拋異常。

當一個物件由於沒有相應的方法實現而無法響應某訊息時,執行時系統將通過forwardInvocation:訊息通知該物件。每個物件都從NSObject類中繼承了forwardInvocation:方法。然而,NSObject中的方法實現只是簡單地呼叫了doesNotRecognizeSelector:。通過實現我們自己的forwardInvocation:方法,我們可以在該方法實現中將訊息轉發給其它物件。

forwardInvocation:方法就像一個不能識別的訊息的分發中心,將這些訊息轉發給不同接收物件。或者它也可以象一個運輸站將所有的訊息都發送給同一個接收物件。它可以將一個訊息翻譯成另外一個訊息,或者簡單的”吃掉“某些訊息,因此沒有響應也沒有錯誤。forwardInvocation:方法也可以對不同的訊息提供同樣的響應,這一切都取決於方法的具體實現。該方法所提供是將不同的物件連結到訊息鏈的能力。

注意: forwardInvocation:方法只有在訊息接收物件中無法正常響應訊息時才會被呼叫。 所以,如果我們希望一個物件將negotiate訊息轉發給其它物件,則這個物件不能有negotiate方法。否則,forwardInvocation:將不可能會被呼叫。

轉發和多繼承

轉發和繼承相似,可以用於為Objc程式設計新增一些多繼承的效果。就像下圖那樣,一個物件把訊息轉發出去,就好似它把另一個物件中的方法借過來或是“繼承”過來一樣。

這使得不同繼承體系分支下的兩個類可以“繼承”對方的方法,在上圖中WarriorDiplomat沒有繼承關係,但是Warriornegotiate訊息轉發給了Diplomat後,就好似DiplomatWarrior的超類一樣。

訊息轉發彌補了 Objc 不支援多繼承的性質,也避免了因為多繼承導致單個類變得臃腫複雜。它將問題分解得很細,只針對想要借鑑的方法才轉發,而且轉發機制是透明的。

替代者物件(Surrogate Objects)

轉發不僅能模擬多繼承,也能使輕量級物件代表重量級物件。弱小的女人背後是強大的男人,畢竟女人遇到難題都把它們轉發給男人來做了。這裡有一些適用案例,可以參看官方文件

轉發與繼承

儘管轉發很像繼承,但是NSObject類不會將兩者混淆。像respondsToSelector: 和 isKindOfClass:這類方法只會考慮繼承體系,不會考慮轉發鏈。比如上圖中一個Warrior物件如果被問到是否能響應negotiate訊息:

1
2
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...

結果是NO,儘管它能夠接受negotiate訊息而不報錯,因為它靠轉發訊息給Diplomat類來響應訊息。

如果你為了某些意圖偏要“弄虛作假”讓別人以為Warrior繼承到了Diplomatnegotiate方法,你得重新實現respondsToSelector: 和 isKindOfClass:來加入你的轉發演算法:

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}

除了respondsToSelector: 和 isKindOfClass:之外,instancesRespondToSelector:中也應該寫一份轉發演算法。如果使用了協議,conformsToProtocol:同樣也要加入到這一行列中。類似地,如果一個物件轉發它接受的任何遠端訊息,它得給出一個methodSignatureForSelector:來返回準確的方法描述,這個方法會最終響應被轉發的訊息。比如一個物件能給它的替代者物件轉發訊息,它需要像下面這樣實現methodSignatureForSelector:

1
2
3
4
5
6
7
8
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}

健壯的例項變數(Non Fragile ivars)

在 Runtime