概要:傻瓜式講解動(dòng)態(tài)綁定和消息轉(zhuǎn)發(fā)。
學(xué)習(xí)進(jìn)度:
- runtime小序曲,從運(yùn)行時(shí)多態(tài)看這股神秘力量
- runtime進(jìn)行曲,objc_msgSend的前世今生(一)
- runtime進(jìn)行曲,objc_msgSend的前世今生(二)
- runtime變奏曲,那些藏在runtime中的接口(一)
- runtime變奏曲,那些藏在runtime中的接口(二)
一、objc_msgSend偽代碼復(fù)習(xí)
偽代碼
// 首先看一下objc_msgSend的方法實(shí)現(xiàn)的偽代碼
id objc_msgSend(id self, SEL op, ...) {
if (!self) return nil;
// 關(guān)鍵代碼(a)
IMP imp = class_getMethodImplementation(self->isa, SEL op);
imp(self, op, ...); // 調(diào)用這個(gè)函數(shù),偽代碼...
}
// 查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
if (!cls || !sel) return nil;
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) {
... // 執(zhí)行動(dòng)態(tài)綁定
}
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) return _objc_msgForward; // 這個(gè)是用于消息轉(zhuǎn)發(fā)的
return imp;
}
// 遍歷繼承鏈,查找IMP
IMP lookUpImpOrNil(Class cls, SEL sel) {
if (!cls->initialize()) {
_class_initialize(cls);
}
Class curClass = cls;
IMP imp = nil;
do { // 先查緩存,緩存沒有時(shí)重建,仍舊沒有則向父類查詢
if (!curClass) break;
if (!curClass->cache) fill_cache(cls, curClass);
imp = cache_getImp(curClass, sel);
if (imp) break;
} while (curClass = curClass->superclass); // 關(guān)鍵代碼(b)
return imp;
}
問題
偽代碼中大部分在runtime進(jìn)行曲,objc_msgSend的前世今生(一)中已經(jīng)說明的很詳細(xì),這里看下上篇中留下的兩個(gè)小疑問:
- 動(dòng)態(tài)綁定。
- 消息轉(zhuǎn)發(fā)。
二、動(dòng)態(tài)綁定
動(dòng)態(tài)綁定,從名稱來看就大致懂了。如果調(diào)用一個(gè)類的方法,而這個(gè)類及其父類均沒有實(shí)現(xiàn)這個(gè)方法。那么我們就在運(yùn)行時(shí)綁定此方法到該類。舉一例子如下:
// 使用@dynamic表明不自動(dòng)合成屬性a的set和get方法。
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@property (nonatomic, assign) NSInteger a;
@end
@implementation A
@dynamic a;
@end
int main(int argc, char * argv[]) {
A *aObject = [[A alloc] init];
NSLog(@"%ld", aObject.a); // 崩于此行
}
// 執(zhí)行結(jié)果:crash,報(bào)錯(cuò)如下
2017-01-09 21:25:06.929 block[28341:228218] -[A a]: unrecognized selector sent to instance 0x60800000c580
參照一中objc_msgSend執(zhí)行步驟,可知A類并沒有動(dòng)態(tài)綁定和消息轉(zhuǎn)發(fā),所以返回的imp為空,執(zhí)行crash。下面我們?yōu)槠浼尤雱?dòng)態(tài)綁定的方法。
// 代碼
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@property (nonatomic, assign) NSInteger a;
@end
@implementation A
@dynamic a;
int a(id self, SEL _cmd) {
return 1;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
class_addMethod([self class], @selector(a), (IMP)a, "i@:");
return YES;
}
@end
int main(int argc, char * argv[]) {
A *aObject = [[A alloc] init];
NSLog(@"%ld", aObject.a);
}
// 沒有crash,并輸出1
2017-01-09 21:50:11.314 block[30605:247164] 1
OC中的方法實(shí)質(zhì)上就是一個(gè)有id self和 SEL _cmd兩個(gè)參數(shù)的C方法。
這里的aObject.a中a為實(shí)例方法,那么類方法怎么進(jìn)行動(dòng)態(tài)綁定?即通過resolveClassMethod方法。
// 代碼
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@end
@implementation A
void b(id self, SEL _cmd) {
NSLog(@"b");
}
+ (BOOL)resolveClassMethod:(SEL)sel {
class_addMethod([self class], @selector(b), (IMP)b, "v@:");
return YES;
}
@end
int main(int argc, char * argv[]) {
[[A class] performSelector:@selector(b)]; // 因?yàn)閇A b];這種調(diào)用方式會(huì)編譯錯(cuò)誤,所以動(dòng)態(tài)調(diào)用
}
// 打斷點(diǎn)查看,雖然調(diào)用了resolveClassMethod,但還是crash
017-01-09 22:26:58.671 block[34171:285065] +[A b]: unrecognized selector sent to class 0x1037b5e30
參照上一篇文章中,A的class中只存有實(shí)例方法,A的metaClass中只存有類方法,而相應(yīng)的調(diào)用也是如此,即實(shí)例方法在A的class中找,而類方法在A的metaClass中找。所以上述給A class添加方法b并沒有作用,僅僅是添加了一個(gè)實(shí)例方法b。正確方法如下:
// 代碼
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface A : NSObject
@end
@implementation A
void b(id self, SEL _cmd) {
NSLog(@"b");
}
+ (BOOL)resolveClassMethod:(SEL)sel {
Class aMeta = objc_getMetaClass(class_getName([self class]));
class_addMethod([aMeta class], @selector(b), (IMP)b, "v@:");
return YES;
}
@end
int main(int argc, char * argv[]) {
[[A class] performSelector:@selector(b)];
}
// 沒有crash,輸出b,無敵
2017-01-09 22:31:53.440 block[34634:289598] b
三、消息轉(zhuǎn)發(fā)
參見一中查找IMP代碼。
// 查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
if (!cls || !sel) return nil;
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) {
... // 執(zhí)行動(dòng)態(tài)綁定
}
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) return _objc_msgForward; // 這個(gè)是用于消息轉(zhuǎn)發(fā)的
return imp;
}
可知,消息轉(zhuǎn)發(fā)是objc_msgSend的最后一道防線。如果找不到imp,則會(huì)調(diào)用下面方法拋出異常。
- (void)doesNotRecognizeSelector:(SEL)aSelector;
而在調(diào)用doesNotRecognizeSelector之前,會(huì)先調(diào)用方法(對(duì)于實(shí)例方法)。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
若此方法能為另一個(gè)類的消息創(chuàng)建一個(gè)有效的方法簽名(當(dāng)另一個(gè)類中有aSelector則可以創(chuàng)建)。創(chuàng)建方式如下:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
B *bObject = [[B alloc] init]; // 假設(shè)B中有實(shí)例方法aSelector
signature = [bObject methodSignatureForSelector:aSelector];
}
return signature;
}
若返回值signature為nil,則執(zhí)行doesNotRecognizeSelector拋出異常,若signature簽名成功,則執(zhí)行轉(zhuǎn)發(fā)方法。
- (void)forwardInvocation:(NSInvocation *)anInvocation {
B *bObject = [[B alloc] init];
[anInvocation invokeWithTarget:bObject];
}
當(dāng)然,進(jìn)入forwardInvocation中之后就不會(huì)在調(diào)用起本類的doesNotRecognizeSelector方法了。除非將這個(gè)消息又轉(zhuǎn)回自己(如果調(diào)用某個(gè)對(duì)象的方法沒找到,則調(diào)用相應(yīng)類的doesNotRecognizeSelector拋出異常)。又比如,若forwardInvocation什么都不寫,則不會(huì)有任何現(xiàn)象,也不會(huì)crash,也不會(huì)拋出異常。
下面看一下完整的實(shí)例方法轉(zhuǎn)發(fā)代碼:
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface B : NSObject
- (void)b;
@end
@implementation B
- (void)b {
NSLog(@"b");
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
[super doesNotRecognizeSelector:aSelector];
}
@end
@interface A : NSObject
@end
@implementation A
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
B *bObject = [[B alloc] init];
signature = [bObject methodSignatureForSelector:aSelector];
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
B *bObject = [[B alloc] init];
[anInvocation invokeWithTarget:bObject];
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
[super doesNotRecognizeSelector:aSelector];
}
@end
int main(int argc, char * argv[]) {
A *aObject = [[A alloc] init];
[aObject performSelector:@selector(b)];
}
實(shí)例方法的轉(zhuǎn)發(fā)大概講完了,接下來看下類方法的轉(zhuǎn)發(fā)。和實(shí)例方法類似,有兩個(gè)需要額外注意的地方。
- 實(shí)例方法是在B的class中查找b方法,若查找類方法,需要在B的metaClass中查找。
- 上述代碼中的methodSignatureForSelector、forwardInvocation、doesNotRecognizeSelector在類方法的轉(zhuǎn)發(fā)過程不會(huì)被觸發(fā),需要將前面的“-”換成“+”才會(huì)被觸發(fā)(畢竟是查找類方法,有點(diǎn)區(qū)別)。
類方法轉(zhuǎn)發(fā)代碼如下:
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface B : NSObject
+ (void)b;
@end
@implementation B
+ (void)b {
NSLog(@"b");
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
[super doesNotRecognizeSelector:aSelector];
}
@end
@interface A : NSObject
@end
@implementation A
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
Class bMeta = objc_getMetaClass(class_getName([B class]));
signature = [[bMeta class] instanceMethodSignatureForSelector:aSelector];
}
return signature;
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation invokeWithTarget:[B class]];
}
+ (void)doesNotRecognizeSelector:(SEL)aSelector {
[super doesNotRecognizeSelector:aSelector];
}
@end
int main(int argc, char * argv[]) {
[[A class] performSelector:@selector(b)];
}
四、消息轉(zhuǎn)發(fā)補(bǔ)充
1、forwardingTargetForSelector
看到現(xiàn)在,不少曾經(jīng)看過消息轉(zhuǎn)發(fā)的一些文章的讀友可能發(fā)現(xiàn),有一個(gè)函數(shù)沒有被提到:
- (id)forwardingTargetForSelector:(SEL)aSelector;
那么,它是做什么的呢?經(jīng)過測(cè)試,該函數(shù)會(huì)在methodSignatureForSelector調(diào)用之前進(jìn)行調(diào)用,來看一下是否可以進(jìn)行轉(zhuǎn)發(fā)。下面寫一個(gè)forwardingTargetForSelector實(shí)現(xiàn)轉(zhuǎn)發(fā)的樣例:
// 下述為類方法的轉(zhuǎn)發(fā)樣例,如果是實(shí)例方法,需要將forwardingTargetForSelector改為實(shí)例方法
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface B : NSObject
+ (void)b;
@end
@implementation B
+ (void)b {
NSLog(@"b");
}
@end
@interface A : NSObject
@end
@implementation A
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(b)) {
return [B class];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
int main(int argc, char * argv[]) {
[[A class] performSelector:@selector(b)];
}
// 輸出
2017-01-10 08:54:39.007 block[52966:479279] b
那么,既然forwardingTargetForSelector可以實(shí)現(xiàn)消息轉(zhuǎn)發(fā),為什么還要使用forwardInvocation作為消息管理中心呢?
- 雖然,forwardingTargetForSelector使用簡(jiǎn)單,不需要重寫methodSignatureForSelector,產(chǎn)生的消耗也比forwardInvocation低得多。
- 但是,forwardingTargetForSelector無法獲取當(dāng)前的NSInvocation,或者說少了一些可以操作的值。
2、respondsToSelector:和isKindOfClass:
若不進(jìn)行重寫,respondsToSelector:和isKindOfClass:均只會(huì)作用于繼承鏈,而不會(huì)觸及轉(zhuǎn)發(fā)。假設(shè)我在四.1中:
// 代碼
NSLog(@"%i", [[A class] respondsToSelector:@selector(b)]);
// 雖然A中實(shí)現(xiàn)了b方法的轉(zhuǎn)發(fā),但是respondsToSelector:并不會(huì)查看
2017-01-10 09:10:47.921 block[54479:495382] 0
當(dāng)然,我們可以重寫respondsToSelector:來保證消息轉(zhuǎn)發(fā)鏈也可以響應(yīng)。
// 代碼,前面為+是因?yàn)檫@里看的是類方法b
+ (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
if ([[B class] respondsToSelector:@selector(b)])
return YES;
}
return NO;
}
// 調(diào)用NSLog(@"%i", [[A class] respondsToSelector:@selector(b)]);輸出
2017-01-10 09:14:59.227 block[54899:500487] 1
同樣的道理,isKindOfClass:、instancesRespondToSelector:和 conformsToProtocol:都會(huì)有相同的機(jī)制。
五、消息轉(zhuǎn)發(fā)應(yīng)用
1、多重繼承
根據(jù)上述任意消息轉(zhuǎn)發(fā)樣例,可知實(shí)現(xiàn)了在A中調(diào)用B中的方法b,大概可以猜到,這是不是類似繼承機(jī)制?答案是肯定的,因?yàn)镺C不支持多繼承,此處就給了一個(gè)實(shí)現(xiàn)多繼承的方式,因?yàn)槲覀兛梢詫?shí)現(xiàn)任意個(gè)類消息的轉(zhuǎn)發(fā),這里就不舉例了。
這是一個(gè)先進(jìn)的技術(shù),只適用于沒有其他解決方案的情況下。它能作為繼承的替代品。如果必須使用這種技術(shù),請(qǐng)確保您充分了解轉(zhuǎn)發(fā)的類和被轉(zhuǎn)發(fā)的類的行為。
2、NSProxy使用和面向切面編程
暫時(shí)沒空寫這部分,參見神經(jīng)病院Objective-C Runtime住院第二天——消息發(fā)送與轉(zhuǎn)發(fā)第四節(jié)。
3、JSPatch
JSPatch中也用到消息轉(zhuǎn)發(fā)相關(guān)內(nèi)容,暫不介紹,后續(xù)后有專門的文章。
六、消息傳遞流程圖

七、文獻(xiàn)
1、https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html#//apple_ref/doc/uid/TP40008048-CH105-SW1
2、http://www.itdecent.cn/p/4d619b097e20