iOS开发-runtime-消息传递和转发机制

objective-c是一门动态语言,动态语言的特点就是方法的调用是在运行时(runtime)而不是编译时。

C语言就是一门典型的静态语言,在编译时程序就已经知道要调用的函数,所以在C语言中如果某个函数没有实现,是没有办法编译成功的。

但是OC不同,在编译时并没有确定的知道要调用的函数,甚至于在运行时还可以动态的添加方法,但是这也带来了问题,就是只有在程序运行起来之后,我们才会发现某些方法没有实现,而这种情况往往导致了程序的崩溃。

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

那接下来我们就来彻底的了解一下这门动态语言中的消息是如何传递和转发的。

objc_msgSend() 函数

说到消息传递机制,最应该说的就是大名鼎鼎的objc_msgSend()函数了。相信写过一段时间OC代码的同学们应该都有或多或少的听说过这个方法。

我们每天写的OC代码,在编译器看到时都会转换成一条C语言函数调用。

我们先来看一下objc_msgSend的结构是什么样的。

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

咋一看好像有点懵,这都什么对什么呀。没关系,我们用OC代码带进去看一下就明白了。

比如在OC中的这个方法:

id returnValue = [someObject messageName:parameter];

在编译器中就变成了:

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

这下是不是清楚很多啦,接下来我们再来看看每个参数都是什么意思,还是用上边的这个例子:

  • someObject为方法的调用者,也可以叫做接受者(receiver);
  • messageName:是方法名,也就是选择子(selector);
  • parameter是参数,参数可以是一个,也可以是多个;
  • messageName和参数合起来叫做消息(message);

了解了这些参数的含义之后,我们来看一下消息传递的流程。

消息传递流程

首先接受者根据isa指针找到接受者自己的类,每个类都会有自己的方法列表(method list),然后会在这个方法列表中遍历。

如果这时候找到合适的方法名,就会根据IMP指针跳转到方法的实现代码,调用方法的实现。

如果没有找到合适的方法名,接受者会根据所属类的父类(superClass)指针,在父类中遍历寻找方法名,如果父类没有,就去父类的父类寻找,一级一级向上。

如果在父类们中找到了合适的方法名,就会跳转到方法的实现代码,调用方法的实现。

如果还没有找到合适的方法,就会执行消息转发(message forwarding)

消息转发

消息转发其实我们已经见过了。

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

之前说的这段代码就是消息转发给了NSObjectdoesNotRecognizeSelector:方法抛出来的,意思就是接受者ViewController无法响应btnClicked这个消息,最后转发给了最最最父类NSObject处理,而NSObject也无法处理,就抛出了异常。

消息转发流程

消息转发一共分为三个阶段

  • 1.第一阶段为动态方法解析(dynamic method resolution),就是看看能否动态添加方法处理当前无法解读的消息,如果此时无法动态新增方法,则进入第二阶段。
  • 2.第二阶段为备援接受者(replacement receiver),也就是看看运行时系统会请求当前接受者让其他接受者处理这条消息,如果还没有办法,则进入第三阶段。
  • 3.第三阶段为完整消息转发机制,运行时系统会把消息有关的细节全部封装到NSInvocation对象中,再给接受者最后一次机会,如果还没有办法解决,就只能抛出异常了。

动态方法解析

这是整个消息转发流程的第一个阶段,如果在收到无法响应的消息后,会调用所属类的方法:

//实例对象
+ (BOOL)resolveInstanceMethod:(SEL)selector
//类对象
+ (BOOL)resolveClassMethod:(SEL)selector

其中参数selector为未处理的方法。

返回值@return表示能否新增一个方法来处理,一般使用@dynamic属性来实现:

/************** 使用 resolveInstanceMethod 实现 @dynamic 属性 **************/
id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);
+ (BOOL)resolveInstanceMethod:(SEL)selector
{
    NSString *selectorString = NSStringFromSelector(selector);
    if (/* selector is from a @dynamic property */)
    {
        if ([selectorString hasPrefix:@"set"])
        {
            // 添加 setter 方法
            class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
        }
        else
        {
            // 添加 getter 方法
            class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
        }
        return YES;
    }
    return [super resolveInstanceMethod:selector];
}

备援接受者

这是整个消息转发机制的第二站,看名字就可以看出来,这是在寻找一个备用援救的接受者,到了这一阶段,系统会调用这个方法:

- (id)forwardingTargetForSelector:(SEL)aSelector;

传入参数aSelector同样为无法处理的方法。

返回值为当前找到的备援接受者,如果没有则返回nil,进入下一阶段。

完整的消息转发机制

如果前两个阶段都没有办法处理消息,就会启动完整的消息转发机制。

首先会创建NSInvocation对象,把尚未处理的那条消息的全部信息细节装在里边,在触发NSInvocation对象时,系统派发系统(message-dispatch system)将会把消息指派给目标对象。这时会调用该方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation;

传入的参数anInvocation就包含了消息的所有内容。

如果此时还是没办法处理消息,就会沿着继承的顺序一步一步向父类调用相同的方法,直到最后的NSObject类中,这时候如果还没有办法处理消息,就会调用doesNotRecognizeSelector:抛出异常,之前看到的崩溃原因就是这个了。

到此为止,消息转发的整个流程就都结束了。

所以,利用消息转发机制,在程序崩溃之前,我们还有三次机会来拯救我们的应用,避免崩溃的事件发生。

Demo

最后放个Demo吧,代码比较简单,就不放地址了,直接把代码贴上来。

EOCAutoDictionary.h

#import <Foundation/Foundation.h>

@interface EOCAutoDictionary : NSObject

@property (nonatomic ,strong) NSString          *string;
@property (nonatomic ,strong) NSNumber          *number;
@property (nonatomic ,strong) NSDate            *date;
@property (nonatomic ,strong) id                 opaqueObject;

@end

EOCAutoDictionary.m

#import "EOCAutoDictionary.h"
#import <objc/runtime.h>

@interface EOCAutoDictionary ()

@property (nonatomic ,strong) NSMutableDictionary           *backingStore;

@end


@implementation EOCAutoDictionary

@dynamic string, number, date, opaqueObject;

- (id)init
{
    self = [super init];
    
    if(self)
    {
        _backingStore = [[NSMutableDictionary alloc] init];
    }
    return self;
}


//这里注意实例方法和类方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSString *selectorStirng = NSStringFromSelector(sel);
    
    if([selectorStirng hasPrefix:@"set"])
    {
        class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
    }
    else
    {
        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
    }
    
    return YES;
}


id autoDictionaryGetter(id self, SEL _cmd)
{
    EOCAutoDictionary *typeSelf = (EOCAutoDictionary *)self;
    NSMutableDictionary *backingStore = typeSelf.backingStore;
    
    NSString *key = NSStringFromSelector(_cmd);
    
    return [backingStore objectForKey:key];
}

void autoDictionarySetter(id self ,SEL _cmd, id vaule)
{
    EOCAutoDictionary *typeSelf = (EOCAutoDictionary *)self;
    
    NSMutableDictionary *backingStore = typeSelf.backingStore;
    
    //获取方法名
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    
    //去除尾部“:”符号
    [key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
    
    //去除头部“set”
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
    NSString *lowercaseFirstChar = [[key substringFromIndex:1] lowercaseString];
    
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    
    if(vaule)
    {
        [backingStore setObject:vaule forKey:key];
    }
    else
    {
        [backingStore removeObjectForKey:key];
    }
}

调用方法:

    EOCAutoDictionary *dict = [[EOCAutoDictionary alloc] init];
    dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
    NSLog(@"dict.date = %@",dict.date);

Demo中使用了@dynamic,但是没有实现变量的setter和getter方法,然后在resolveInstanceMethod:方法中动态的添加了setter和getter方法。

此篇文章大部分内容来自于《Effective Objective-C 2.0》中的第11条和第12条,仅为作者学习使用,如果有什么地方写的不对,还请大佬们多多指教。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容