iOS Runtime 运行时之 消息转发

unrecognized selector sent to instance 0x604000010c60是我们最常见的错误,是由于调用的方法在接受者中没有实现导致的。但是在控制台中输出这句log之前,运行时系统究竟做了哪些操作呢?我们有没有机会在程序崩溃之前力挽狂澜呢?

答案是肯定的,接下来我们一起学习下消息从发送到输出崩溃log我们都可以做哪些事。

#示例代码
#import <Foundation/Foundation.h>
@interface Dog : NSObject
- (void)bark:(NSString *)bark;
@end

#import "Dog.h"
#import "AnotherDog.h"
#import <objc/runtime.h>
@implementation Dog
@end


#import <Foundation/Foundation.h>
@interface AnotherDog : NSObject
- (void)bark:(NSString *)bark;
@end

#import "AnotherDog.h"
@implementation AnotherDog
- (void)bark:(NSString *)bark{
    NSLog(@"\nAnotherDog: %@\n",bark);
}
@end

一、消息发送

在OC中,方法调用称为向对象发送消息。

#我们给dog发送一个只有声明并未实现的消息。
[dog bark:@"汪汪"];

那么,[dog bark:@"汪汪"]编译后是什么呢?

objc_msgSend(dog, @selector(bark:), @"汪汪");

objc_msgSend的具体流程如下:

1、通过isa指针找到所属类(参考下图),
2、查找类的cache列表, 如果没有则下一步,
3、查找类的“方法列表”,
4、如果能找到名称相符的方法, 就跳至其实现代码,
5、若找不到, 就沿着继承顺序继续向上查找,
6、如果能找到与方法名相符的方法, 就跳至其实现代码,
7、若找不到, 进入动态方法解析阶段,允许动态给未实现的方法添加方法实现
8、若在动态解析阶段未进行处理,最终进入“消息转发”流程。
9、在转发流程中仍然没有找到能够处理消息的接收者,会抛出unrecognized selector sent to instance异常。

一张类和对象的继承关系的经典图

二、动态方法解析(第一次挽救崩溃的机会)

官方文档说,在启用消息转发机制之前,首先会进行动态方法解析。
这个方法允许通过class_addMethod动态给要处理的sel添加实现。

如果respondstoselector,ismemberofclass:或instancesRespondToSelector:被调用,动态方法是有机会先给方法选择器提供一个IMP。

#一个是处理对象方法,一个处理类方法
//sel :要处理的方法选择器
//return :如果找到方法并且添加给接收者则返回YES, 否则返回NO启用消息转发机制
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel

动态添加实现如下:

#import "Dog.h"
#import "AnotherDog.h"
#import <objc/runtime.h>

void dynamicMethodIMP(id self, SEL _cmd)
{
    // implementation ....
    NSLog(@"我是动态添加的实现");
}

@implementation Dog

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(bark:))
    {
       //如果是未实现的bark:方法 则将dynamicMethodIMP的实现添加给bark:
        class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

@end

控制台

2018-02-23 16:10:09.188290+0800 runtimeDemo[87666:1689163] 我是动态添加的实现

【注意】关于方法的返回值,如果仅仅是返回YES并不能阻止方法继续进入消息转发流程。只有给bark:添加了方法实现并且返回YES,否则继续进入消息转发流程。

三、消息转发

究竟什么是消息转发呢?
向不能处理该消息的对象发送消息会导致一个错误。然而,在报错之前,运行时系统给接收对象一个处理消息的第二次机会。这就是消息转发。

1、 尝试寻找其他接收者(第二次挽救崩溃的机会)

请接收者看看有没有其他对象能处理这条消息? 如果有, 则把消息转给那个对象, 消息转发结束.

动态方法解析失败, 则调用这个方法

- (id)forwardingTargetForSelector:(SEL)selector
// selector : 那个未知的消息
// 返回一个能响应未知方法选择器的对象
#import "Dog.h"
#import "AnotherDog.h"
#import <objc/runtime.h>

void dynamicMethodIMP(id self, SEL _cmd)
{
    // implementation ....
    NSLog(@"我是动态添加的实现");
}

@implementation Dog

+ (BOOL)resolveInstanceMethod:(SEL)sel{

    return NO;
}
#返回非空,非self的对象会终止消息转发。
- (id)forwardingTargetForSelector:(SEL)aSelector{
    AnotherDog * anotherDog =  [[AnotherDog alloc] init];
    if ([anotherDog respondsToSelector:aSelector]) {
        return anotherDog;
    }
    return nil;
}

@end

控制台

2018-02-23 18:06:21.359926+0800 runtimeDemo[13562:1928673] 
AnotherDog: 汪汪
2、 完整的消息转发(第三次挽救崩溃的机会)

找不到其他接收者, 这个方法就准备要被包装成一个NSInvocation对象, 在这里要先返回一个方法签名,如果方法签名返回nil,转发终止,程序崩溃。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

最后一次机会处理这个方法, 搞不定就直接程序崩溃!

// invocation : 封装了未知消息所有相关细节的对象
- (void)forwardInvocation:(NSInvocation *)invocation
#import "Dog.h"
#import "AnotherDog.h"
#import <objc/runtime.h>

void dynamicMethodIMP(id self, SEL _cmd)
{
    // implementation ....
    NSLog(@"我是动态添加的实现");
}

@implementation Dog
+ (BOOL)resolveInstanceMethod:(SEL)sel{

    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSMethodSignature *signature;
    AnotherDog * anotherDog =  [[AnotherDog alloc] init];
    signature = [anotherDog methodSignatureForSelector:aSelector];
    if (signature) return signature;
   
    //如果有其他能够相应的对象,那么生成对应的签名
    //signature = [otherDog methodSignatureForSelector:aSelector];
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    AnotherDog * anotherDog =  [[AnotherDog alloc] init];
    if ([anotherDog respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:anotherDog];
    }
}

控制台

2018-02-24 14:45:54.098264+0800 runtimeDemo[75920:3959070] 
AnotherDog: 汪汪

综上当调用我们一个未实现的方法时,我们有三次挽救的机会。如果不做处理,系统默认调用doesNotRecognizeSelector方法,抛出unrecognized selector sent to instance。

如下是系统对于未实现方法的处理方式,更多请下载源码

+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}


+ (id)performSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL))objc_msgSend)((id)self, sel);
}

+ (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)((id)self, sel, obj);
}

+ (id)performSelector:(SEL)sel withObject:(id)obj1 withObject:(id)obj2 {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id, id))objc_msgSend)((id)self, sel, obj1, obj2);
}

- (id)performSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL))objc_msgSend)(self, sel);
}

- (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}

- (id)performSelector:(SEL)sel withObject:(id)obj1 withObject:(id)obj2 {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id, id))objc_msgSend)(self, sel, obj1, obj2);
}


// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject instanceMethodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

+ (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

四、通过消息转发模拟“多继承”

注意这里说的不是真正的多继承,而是通过在消息转发阶段将消息分配给能够真正处理的消息的对象,模拟实现多继承。
官方文档给我们提了思路:
在消息转发阶段的forwardingTargetForSelector或者forwardInvocation将消息分发给真正的接收者去处理。

1、这种处理方式的根本前提是:消息能够进入消息转发流程,那么类本身就不能实现需要“继承”的方法。
2、如果使用转发机制来扩展类的功能,则转发机制应该像继承一样透明。我们希望对象表现得好像它们确实继承了它们转发消息的对象的行为,那么需要重新实现respondsToSelector:和isKindOfClass:方法来包含我们的转发算法:

- (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,ismemberofclass:和isKindOfClass: ,instancesRespondToSelector:方法也应该实现转发算法。如果使用了协议,那么conformsToProtocol:方法也应该重写。相应的也需要重写methodSignatureForSelector:方法使得可以返回准确的方法签名;
例如,如果一个对象可以给代理(surrogate)转发消息

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

至此,本章内容结束,欢迎批评交流。

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

推荐阅读更多精彩内容