Runtime之动态方法解析和转发

前言

在Objective-C中,如果只在头文件中声明了方法,但没有在m文件中实现该方法,如果调用该方法,通常情况下程序会崩溃并抛出unrecognized selector sent to instance的异常。

而在异常抛出之前,Runtime会给你三次拯救程序的机会:

  1. Method resolution
  2. Fast forwarding
  3. Normal forwarding

下图是动态方法解析和消息转发的图

10.png

1. Method resolution

Runtime首先给我们提供的一次机会,动态方法解析,可以让我们在运行时完成向特定类添加特定方法实现的操作。如下例子:

@interface TestClass : NSObject

- (int)print:(int)count;
+ (void)classPrint;

@end

在TestClass类中,我们声明了一个类方法classPrint和实例方法print,但是,并没有在m文件中实现他们。此时编译器会出现警告

Method definition for 'print:' not found

Method definition for 'classPrint' not found

如果在代码中调用这两个方法,编译不会出错,但运行的时候会出错,也就是抛出unrecognized selector sent to instance的异常。

我们使用动态方法解析来在运行时为TestClass类添加这两个方法的实现,其中类方法通过resolveClassMethod来添加,实例方法通过resolveInstanceMethod来添加。

int missingPrint(id obj, SEL _cmd, int count)
{
    NSLog(@"Wow, you found a missing method and count is %d", count);
    return count  * 100;
}

void missingClassPrint(id obj, SEL _cmd)
{
    NSLog(@"调用了missingClassPrint函数");
}

@implementation TestClass

+ (BOOL)resolveClassMethod:(SEL)sel
{
    NSLog(@"调用resolveClassMethod!!!");
    if(sel == @selector(classPrint)) {
        class_addMethod(object_getClass(self), sel, (IMP)missingClassPrint, "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}


+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"调用resolveInstanceMethod!!!");
    if(sel == @selector(print:)) {
        class_addMethod([self class], sel, (IMP)missingPrint, "I@:I");//其中v@:表示方法的参数和返回值,详见Type Encodings
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

@end

上面代码中,我们定义了两个C函数,missingPrint函数用于绑定实例方法,missingClassPrint用于绑定类方法。在resolveClassMethod方法和resolveInstanceMethod方法中,通过class_addMethod方法为类动态添加方法实现。

如此一来,调用classPrint和print:这两个方法时,Runtime会解析成对应的missingClassPrint方法和missingPrint方法。

int main(int argc, const char * argv[]) 
{
    TestClass *t = [[TestClass alloc] init];
    [TestClass classPrint];//这里调用classPrint类方法
    int res = [t print:55];//这里调用print实例方法
    NSLog(@"res = %d", res);

    return 0;
}

PS 1:class_addMethod方法的第三个参数,"v@:",描述的是方法的返回值和传参,该符号的具体内容可在Type Encodings查看

PS 2:resolveClassMethod和resolveInstanceMethod方法都是类方法,在类方法中的self,实际上是Class对象,而不是实例对象,只能调用类方法,不能调用实例方法。

2. Fast forwarding

若在第一步中,类中没有实现动态解析方法,或者在方法中返回NO,则会走到Fast forwarding这一步。

对于实例方法,需要实现forwardingTargetForSelector方法

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(print:)) {
        return [MessageForwarding new];//返回另外一个对象
        //return self  //返回self会导致到第三步Normal forwarding
    }

    return nil;
}

这里可以看到,只要捕捉到方法选择器是print方法,则返回一个新建的MessageForwarding对象,将该消息重定向到MessageForwarding对象来处理。这里MessageForwarding类只需声明并实现了print方法即可。

若想转发类方法,则需要重写forwardingTargetForSelector的类方法

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(classPrint)) {
        return [MessageForwarding class];//这里的返回和实例方法中不一样
    }
    return nil;
}

这里返回的是[MessageForwarding class],因为调用类方法需要的是一个类对象,而不是实例对象。

3.Normal forwarding

当前两步都没有实现,或者在Fast forwarding这一步的forwardingTargetForSelector方法中返回self,都会进入Normal forwarding这一步。

首先会发送-methodSignatureForSelector: 消息获得函数的参数和返回类型。接着Runtime会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。

NSInvocation 实际上就是对一个消息的描述,包括selector 以及参数等信息。

这里需要我们自己实现这两个方法。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];

    if(!methodSignature){
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"I@:I"];
    }

    return methodSignature;
}

//NSInvocation由NSMethodSignature生成
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"calling forwardInvocation: method");
    MessageForwarding *mf = [MessageForwarding new];
    
    if([mf respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:mf];
    }
}

这里仍然是将消息转发给MessageForwarding对象来处理。

总结

Objective-C 中给一个对象发送消息会经过以下几个步骤:

  1. 在对象类的 dispatch table 中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;
  2. 如果没有找到,Runtime 会发送 +resolveInstanceMethod: 或者 +resolveClassMethod: 尝试去 resolve 这个消息;
  3. 如果 resolve 方法返回 NO,Runtime 就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象;
  4. 如果没有新的目标对象返回, Runtime 就会发送 -methodSignatureForSelector:-forwardInvocation: 消息。你可以发送 -invokeWithTarget: 消息来手动转发消息或者发送 -doesNotRecognizeSelector: 抛出异常。

参考文章

Objective-C Runtime
Objective-C Runtime By 杨萧玉

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 既然是消息发送,就存在消息发送失败的时候。那消息发送失败之后,runtime系统会怎么处理呢? 通常情况下,如果消...
    frankisbaby阅读 290评论 0 0
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 目录 Objective-C Runtime到底是什么 Objective-C的元素认知 Runtime详解 应用...
    Ryan___阅读 1,923评论 1 3
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 719评论 0 2
  • 阴天/微风 被室友的关门声砸醒 看了眼手机也到起床的点了 按照以往的每一个步骤 出门前照例检查了背包 公交车还是一...
    程言轻阅读 236评论 0 0