拯救即将崩溃代码之Objective-C消息转发与消息传递

前言

Objective-C是一门非常动态的语言,以至于确定调用哪个方法被推迟到了运行时,而非编译时。与之相反,C语言使用静态绑定,也就是说,在编译期就能决定程序运行时所应该调用的函数,所以在C语言中,如果某个函数没有实现,编译时是不能通过的。而Objective-C是相对动态的语言,运行时还可以向类中动态添加方法,所以编译时并不能确定方法到底有没有对应的实现,编译器在编译期间也就不能报错。

本文将简单介绍消息传递机制和消息转发机制。这需要我们首先了解类对象以及方法列表这个概念,不清楚的同学可以参考笔者之前的文章类的本质-类对象,希望对你有所帮助。本篇文章如有不实之处,还请指正!

(一)对象的消息传递机制 objc_msgSend()

在对象上调用方法在Objective-C中非常普遍。用Objective-C的术语来讲,这叫做“给某个对象发送某条消息”。消息有“名称”或“选择子(selector)”之说。消息可以接受参数,而且还可以有返回值。

在Objective-C中给对象发送消息是如下的格式:

1id returnValue = [someObject messgeName:parameter];

本例中,someObject叫做方法调用者,也叫做接受者(receiver)。messageName:是方法名,也叫做选择子(selector)。选择子与参数合起来叫做”消息“(message)。在运行时,编译器会把上面这个格式的方法调用转化为一条标准的C语言函数调用,该函数就是鼎鼎有名的objc_msgSend(),该函数是消息objc里在运行时传递机制中的核心函数,其原型如下:

1void objc_msgSend(id self, SEL cmd, ...)

这是一个参数个数可变的函数。能接收两个或两个以上的参数,第一个参数代表接受者,第二个参数代表选择子。后续参数就是消息中的那些参数,其顺序不变。接受者就是调用方法的对象或者类(本质上类也是对象,叫做类对象)。选择子就是指的方法的名称,选择子和方法这两个词经常交替使用,其实是指的一个意思。运行时,上面Objc的方法调用会被翻译成一条C语言的函数调用,如下:

1id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter)

消息传递流程

objc_msgSend()函数会一句接受者(调用方法的对象)的类型和选择子(方法名)来调用适当的方法。

接收者会根据isa指针找到接收者自己所属的类,然后在所属类的”方法列表“(method list)中从上向下遍历。如果能找到与选择子名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。

如果找不到与选择子名称相符的方法,接收者会根据所属类的superClass指针,沿着类的继承体系继续向上查找(向父类查找),如果 能找到与名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。

如果在继承体系中还是找不到与选择子相符的方法,此时就会执行”消息转发(message forwarding)“操作。

(二)消息转发流程

上面讲到了对象的消息传递机制,如果在整个类的继承体系中还是找不到与选择子相符的方法,也就是对象或者类对象收到了无法解读的消息,那么就会进入到消息转发环节。

在编译期,向对象或者类对象发送了其无法解读的消息并不会报错,因为在运行期可以继续向类和元类(metaClass)中添加方法,所以编译器在编译期还无法确定类中到底会不会有某个方法的实现。当对象接收到无法解读的消息后,就会启动“消息转发(message forwarding)”机制,我们可以在消息转发过程中告诉对象应该如处理未知消息。

消息转发分为两个阶段。第一阶段叫做“动态方法解析(dynamic method resolution)”,或者叫“动态方法决议”。第二阶段涉及到“完整的消息转发机制(full forwarding mechanism)”,或者叫“完整的消息转发原理”。

(2.1)动态方法解析

动态方法解析的意思就是,征询消息接受者所属的类,看其是否能动态添加方法,以处理当前“这个未知的选择子(unknown selector)“。实例对象在接受到无法解读的消息后,首先会调用其所属类的下列类方法:

1+ (BOOL)resolveInstanceMethod:(SEL)selector

类对象在接受到无法解读的消息后,那么运行期系统就会调用另外的一个方法,如下:

1+ (BOOL)resolveClassMethod:(SEL)selector

如果运行期系统已经执行完了动态方法解析,那么消息接受者自己就无法再以动态新增方法的形式来响应包含该未知选择子的消息了,此时就进入了第二阶段——完整的消息转发。运行期系统会请求消息接受者以其他手段来处理与消息相关的方法调用。

(2.2)完整的消息转发

完整的消息转发又分为两个阶段,第一阶段称为备援接受者(replacement receiver),第二阶段才是启动完整的消息转发机制。

(2.2.1)备援接收者(replacement receiver)

当前接受者如果不能处理这条消息,运行期系统会请求当前接受者让其他接受者处理这条消息,与之对应的方法是:

1- (id)forwardingTargetForSelector:(SEL)selector

方法参数代表未知的选择子,返回值为备援接受者,若当前接受者能找到备援接受者,就直接返回,这个未知的选择子将会交由备援接受者处理。如果找不到备援接受者,就返回nil,此时就会启用”完整的消息转发机制“。

(2.2.2)完整的消息转发

如果转发算法已经来到了这一步,那么代表之前的所有转发尝试都失败了,此时只能启用完整的消息转发机制。完整的消息转发机制是这样的:首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节封装于这个NSInvocation对象中。此对象中包含选择子(selector)、目标(target)及参数。在触发NSInvocation对象时,”消息派发系统(message-dispatch system)“将亲自触发,把消息派发给目标对象。此步骤中会调用下面这个方法来转发消息:

1- (void)forwardInvocation:(NSInvocation *)invocation

消息派发系统触发消息前,会以某种方式改变消息内容,包括 但不限于额外追加一个参数、改变选择子等。

实现此方法时,如果发现调用操作不应该由本类处理,则需要沿着继承体系,调用父类的同名方法,这样一来,继承体系中的每个类都有机会处理这个调用请求,直至rootClass,也就是NSObject类。如果最后调用了NSObject的类方法,那么该方法还会继而调用”doesNotRecognizeSelector:“以抛出异常,此异常表明选择子最终也未能得到处理。消息转发到此结束。

关于does NotRecognizeSelector:你可能感到陌生,但是对于类似于unrecognized selector send to instance xxx这样的错误,你可能并不陌生。这种错误通常是因为调用了某个对象或者某个类里不存在的方法,从而触发了消息转发机制,最终把这个未识别的消息发送给了NSObject的默认实现。

ps:方法缓存

通篇下来,发现调用一个方法并不像我们想的那么简单,更不像我们写的那么简单,一个方法的执行其实底层需要很多步骤。正因如此,objc_msgSend()会将调用过且匹配到的方法缓存在”快速映射表(fast map)“中,快速映射表就是方法的缓存表。每个类都有这样一个缓存。所以,即便子类实例从父类的方法列表中取过了某个对象方法,那么子类的方法缓存表中也会缓存父类的这个方法,下次调用这个方法,会优先去当前类(对象所属的类)的方法缓存表中查找这个方法,这样的好处是显而易见的,减少了漫长的方法查找过程,使得方法的调用更快。同样,如果父类实例对象调用了同样的方法,也会在父类的方法缓存表中缓存这个方法。

同理,如果用一个子类对象调用某个类方法,也会在子类的metaclass里缓存一份。而当用一个父类对象去调用那个类方法的时候,也会在父类的metaclass里缓存一份。

拯救

日常开发中,有时调用对象的某个方法,会出现异常,如下:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController doSomething:]: unrecognized selector sent to instance 0x7fcd815bc580'

意思是在类ViewController中调用doSomething方法,但是没有找到该方法,程序异常,然后崩溃。

对于这种情况,有没有什么补救措施防止程序崩溃呢?

答案是有,方式大致有几种:

1、用try...catch来包住可能会出问题的代码;

2、调方doSomething方面前先用respondsToSelector来判断有无指定的方法,有再调用;

3、NSSetUncaughtExceptionHandler机制来控制处理;

4、使用Objective-C Runtime的消息转发机制来处理。
处理方式 说明
1 用try...catch来包住可能会出问题的代码;
@try {
<#Code that can potentially throw an exception#>
}
@catch (NSException *exception) {
<#Handle an exception thrown in the @try block#>
}
@finally {
<#Code that gets executed whether or not an exception is thrown#>
}
2 调方doSomething方面前先用respondsToSelector来判断有无指定的方法,有再调用;
if ([类的实例对象 respondsToSelector:@selector(doSomething:)]) {
[类的实例对象 doSomething:@"xxx"];
}
3 NSSetUncaughtExceptionHandler机制来控制处理; NSSetUncaughtExceptionHandler(&ocExceptionHandle);
void ocExceptionHandle(NSException *exception)
{
//...
}
4 使用Objective-C的消息转发机制来处理。
1.+ resolveInstanceMethod:(SEL)sel // 对应实例方法
或者 + resolveClassMethod:(SEL)sel // 对应类方法
2.- (id)forwardingTargetForSelector:(SEL)aSelector
3.- (void)forwardInvocation:(NSInvocation *)anInvocation
说明下:

对于方法1,try...catch可以抓住上述NSInvalidArgumentException异常。try...catch适用于未来不可知的情况,但是对于知道可能会有问题的地方,还是需要编码时加强健壮性,就像上面的方法2,respondsToSelector就是用来判断是否存在指定的方法,如果存在再调用就不会发生错误。

对于方法3,适用于我们没有写try...catch的代码,如果发生异常,就会被我们指定的异常Handler抓住,这个时候就可以做相关处理,比如将异常信息记日志、发送服务器或者忽略等操作。

而方法4,消息转发(Message Forward),是Objective-C语言级别的特性,即Runtime的特性,也是Objective-C动态语言的特色之一。Objective-C Runtime会提供3次挽救的机会(准确的说是1次动态方法解析+2次消息转发;其中实例方法救3次,类方法救1次):
正式名称 通俗理解 实例方法 类方法
1 Dynamic Method Resolution
(动态方法解析) 第一次挽救:Runtime说给我一个可以用的替代方法。 有 有
2 Fast Forwarding
(快速消息转发,指定备用接受者) 第二次挽救:Runtime说告诉我谁可以搞定这个事。 有
3 Normal Message Forward
(消息转发) 第三次挽救:Runtime说我把这个烂事包装成NSInvocation给你,你看着办吧。 有
补充一点:

1、Objective-C实例对象调用一个方法,会首先在本类方法列表查找,如果没有,会在父类再查找,直到根类NSObject,在任何一层找到方法,则执行,如果到了最后根类NSObject还没有找到,才会触发Objective-C Runtime的消息转发机制。

2、Objective-C类对象调用一个方法,会首先在本类的元类方法列表中查找,如果没有,会在元类的父类再查找,直到根类NSObject的元类,在任何一层找到方法,则执行,如果到了最后根类NSObject元类还没有找到,才会触发Objective-C Runtime的消息转发机制。

(关于Objective-C的类、元类相关知识可以参考我的另一篇文章:Objective-C类之关系)

例子:

有个类ClassA,现在打算调用一个方法doSomething:(NSString *)val,如果这个doSomething方法不存在就会报异常然后程序崩溃;如果在消息转发的阶段我们做了处理就会挽救灾难于崩溃之前。

1、对于实例方法的完整的处理流程如下:
image.png

2、对于类方法的完整的处理流程如下:
image.png

代码例子如下:
ViewController.m
#import "ViewController.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import "Test1.h"
 
@implementation ViewController
 
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    /////////////////////////// 实例方法 ///////////////////////////
    // 1 调用不存在的实例方法“doSomething”
    // 在 resolveInstanceMethod 方法内提供了解救方案,即动态添加了一个方法作为实例方法“doSomething”的替代。
    SEL sel1 = NSSelectorFromString(@"doSomething:");
    [self performSelector:sel1 withObject:nil];
 
    
    // 2 调用不存在的实例方法“doSomething2”
    // 在 forwardingTargetForSelector 方法内提供了解救方案,即将实例化的Test1类对象return给了调用,
    SEL sel2 = NSSelectorFromString(@"doSomething2");
    [self performSelector:sel2 withObject:nil];
    
    // 3 调用不存在的实例方法“doSomething3”
    // 在 forwardInvocation 方法内提供了解救方案,即将实例化的Test1类对象return给了调用,
    SEL sel3 = NSSelectorFromString(@"doSomething3:");
    NSInteger b = 88;
    NSValue *value3 = [NSValue value:&b withObjCType:@encode(NSInteger)];
    [self performSelector:sel3 withObject:value3]; //??传递的参数类型最后Test1里不对。
 
    /////////////////////////// 类方法 ///////////////////////////
    // 4 调用不存在的类方法"doClassMethod1"
    // 在 resolveClassMethod 方法内提供了解救方案,即动态添加了一个方法作为类方法“doClassMethod1”的替代。
    SEL sel4 = NSSelectorFromString(@"doClassMethod1:");
    [ViewController performSelector:sel4 withObject:@"aaa"];
    
    
}
 
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
 
+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@" >> Class resolving %@", NSStringFromSelector(sel));
    if (sel == @selector(doClassMethod1:)) {
 
        IMP imp = imp_implementationWithBlock(^(id self, NSString *val) {
            NSLog(@">>> call doClassMethod1, into block, param=%@", val);
        });
        Class metaClass = object_getClass(self);
        class_addMethod(metaClass, sel, imp, "v@:@@");
        return YES;
    }
    BOOL rev = [super resolveClassMethod:sel];
    return rev;
}
 
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(doSomething:)) {
//        class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *str) {
//            NSLog(@"block, val=%@", str);
//        }), "v@");
        class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:@");
    }
    return [super resolveInstanceMethod:sel];
}
 
 
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(doSomething2)) {
        return [Test1 new]; //Test1.h中没有申明,但Test1.m中有方法就可以。
    }
    return [super forwardingTargetForSelector:aSelector];
}
 
/*
 -forwardInvocation:在一个对象无法识别消息之前调用,再需要重载-methodSignatureForSelector:,因为在调用-forwardInvocation:之前是要把消息打包成NSIvocation对象的,所以需要-methodSignatureForSelector:重载,如果不能在此方法中不能不为空的NSMethodSignature对象,程序依然会崩溃
 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(doSomething3:)) {
        NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
        if (!methodSignature) {
            if ([Test1 instancesRespondToSelector:aSelector]) {
                methodSignature = [Test1 instanceMethodSignatureForSelector:aSelector];
            }
        }
        return methodSignature;
    }
    return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector == @selector(doSomething3:)) {
        Test1 *test1 = [Test1 new];
        if ([test1 respondsToSelector:anInvocation.selector]) {
            [anInvocation invokeWithTarget:test1];
        }
    }
}
 
#pragma mark - Custom Method
 
void dynamicMethodIMP(id self, SEL _cmd, NSString *str) {
 
    NSLog(@"into dynamicMethodIMP, param=%@", str);
    
}
 
@end

Test1.m

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

推荐阅读更多精彩内容