IOS---消息转发机制

一>形成由来:
Object-C是在C的基础上进行编写的一门动态程序语言(也称超C)。底层全部是由C语言实现的。所谓的类,转换到底层C的实现,即结构体等相关的代码。相比于静态编译的语言来说,OC在具体执行的时候才会查询到具体的方法指针,从而实现方法。
而也正是这种OC的动态语言特性造就了两大该语言自身的特色:
1.runtime
2.消息转发机制以及消息传递(msg_send)
本篇幅主要讨论第二种,消息传递与转发机制。
首先要明确的是,OC的方法是如何调用的。在C、C++等语言中,我们定义了一个函数,然后经由其他函数调用时,相当于效用指向所定义函数的指针地址。通过地址查询到了该函数,并随后经由此具体实现一个方法。而OC其实也是相同的。首先需要了解objc_class的结构。
我们所创建的每一个类,以及系统原生定义的工具类,都是遵循如下的class结构体所进行创建和存储的,而OC的Class的isa便指向一个类的结构体,其中包含众多该Class的详细信息:

struct objc_class {
    struct objc_class super_class;  /*父类*/
    const char *name;               /*类名字*/
    long version;                   /*版本信息*/
    long info;                      /*类信息*/
    long instance_size;             /*实例大小*/
    struct objc_ivar_list *ivars;   /*实例参数链表*/
    struct objc_method_list **methodLists;  /*方法链表*/
    struct objc_cache *cache;               /*方法缓存*/
    struct objc_protocol_list *protocols;   /*协议链表*/
};

由该结构可以看到,Class的isa所指向的结构体中,包含了该类的父类名称、该类的名称、版本信息、方法链表等等。而涉及到该Class的所有函数,是由methodLists方法链表进行保存。而OC中的方法存储是用一一对应的映射关系来进行管理的。如下图结构所示:

image.png

如上,方法名和具体的实现地址形成映射的关系。而这里,存储方法名指针selector的是SEL链表,对应存储方法实现地址指针的address的是IMP链表。
SEL:选择子。即方法名的指针,我们平时调用一个方法,都是通过输入方法名,转化为选择子(sel),然后通过该sel在所属类的方法链表中查询具体的方法的。如果查询到,则使用对应的函数地址(imp),查询到具体的实现函数并执行函数。如果查询不到,则涉及到消息的转发了。调用以下方法的时候,我们是能看出方法在调用时的状态的:

[self performSelector:<#(SEL)#>];
    @selector(<#selector#>)

了解完了具体的Object-C的方法调用机制以后,我们就能理解消息转发机制能够形成的基础了。之所以留给消息转发实现的空间,就在于函数的具体执行,是在方法调用才从selector中寻找目标SEL,然后找到对应的IMP中的address来执行的。是动态编译的过程。因为,在Class本身查询SEL出现问题时,留出了一定的空间,供人操作改选择子(SEL)在本类动态添加或者其他对象中查询SEL对应的IMP实现方法,从而实现方法,防止报错。

完整的消息转发机制的过程:


image.png

从图上也可以看得出,消息转发总共分为三个阶段,层层递进。其中,到了第三个阶段,便称之为启动了完整的消息转发机制。当然,我们尽希望于在尽可能靠前的方法中,完成函数的处理,从而减少一定的性能消耗。
首先第一步:类内动态方法解析阶段

void testFunction(id self,SEL _cmd){
    printf("通过动态解析以及runtime添加的方法,并得到执行");
}
//类对象的动态解析函数
+(BOOL)resolveClassMethod:(SEL)sel
{
    //获取方法名称
    NSString * selNameStr = [NSString stringWithFormat:@"%@",NSStringFromSelector(sel)];
    //掩饰一个案例:当我们要识别的是一个testFunction方法,并动态添加
    if ([selNameStr isEqualToString:@"testFunction"])
    {
        //为该类动态添加一个方法testFunction处理消息,并执行该方法
        class_addMethod([self class], sel, (IMP)testFunction, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
//实例对象的动态解析函数
+(BOOL)resolveInstanceMethod:(SEL)sel
{
    //获取方法名称
    NSString * selNameStr = [NSString stringWithFormat:@"%@",NSStringFromSelector(sel)];
    //掩饰一个案例:当我们要识别的是一个testFunction方法,并动态添加
    if ([selNameStr isEqualToString:@"testFunction"])
    {
        //为该类动态添加一个方法testFunction处理消息,并执行该方法
        class_addMethod([self class], sel, (IMP)testFunction, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

当第一步完成了SEL的处理,则方法得到执行,并结束。如若没有处理SEL,则进入第二步,启用备援接受者来处理该SEL。
第二步:启动备援接受者
第二步和第一步的区别在于,备援接受者方法在处理时可以向其他对象发送该SEL,并通过其他对象执行本对象不可执行的方法。这有别于动态方法解析,给自身动态添加未实现方法。当返回 非self\非nil 时,消息被转给新对象执行。

-(id)forwardingTargetForSelector:(SEL)aSelector
{
    NSLog(@"unknow SEL's name is:%@",NSStringFromSelector(aSelector));

    Class unknowClass = NSClassFromString(@"TargetClass");
    NSObject * unknowOC = unknowClass.new;
    if (aSelector == NSSelectorFromString(@"TargetClassFunction")) {
        NSLog(@"找到了备援接受者");
        return unknowOC;
    }
    return [self forwardingTargetForSelector:aSelector];
}

第二步,实质上是使用其他内部对象,来处理本类对象的未知SEL。你也可以利用该特性,营造出多重继承的效果。当第二步返回self、nil时,证明未被查到的SEL还没有找到合适的方法IMP。这是则走到第三步,启动完整的消息转发机制。
第三步:启动完整的消息转发机制
走到这一步时,系统会将那个未被处理的SEL封装为NSInvocation对象。里边详细封装了该SEL的所有信息。
需要注意的是methodSignatureForSelector,和forwardInvocation是配套使用的。如果你不实现方法签名,那么你是调用不到forwardInvocation函数的。这里就和NSInvocation对象本身创建就需要进行方法签名有关。以下是它的结构

/*  NSInvocation.h
    Copyright (c) 1994-2016, Apple Inc. All rights reserved.
*/

#import <Foundation/NSObject.h>
#include <stdbool.h>

@class NSMethodSignature;

NS_ASSUME_NONNULL_BEGIN

NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available")
@interface NSInvocation : NSObject {
@private
    void *_frame;
    void *_retdata;
    id _signature;
    id _container;
    uint8_t _retainedArgs;
    uint8_t _reserved[15];
}

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end
//方法签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSString *sel = NSStringFromSelector(aSelector);
    if ([sel rangeOfString:@"set"].location == 0)
    {
        //动态造一个 setter函数
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    } else {
        //动态造一个 getter函数
        return [NSMethodSignature signatureWithObjCTypes:"@@:"];
    }
}
//完整消息转发
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL selector = [anInvocation selector];
    if ([company respondsToSelector:selector])
    {
        [anInvocation invokeWithTarget:company];
}

第三步,相比较于第二步,有一个显著的特点就是:你可以将该方法封装的NSInvocation对象,同时交给多个不同的对象去尝试实现。

疑问&思考:

.Q:转发机制的三个方法区别在于什么?为什么要层层递进,而不能使用一个万事大吉?
A:首先,方法1是在本类中利用runtime动态去做方法添加来处理;方法2通过转发给另外一个对象来进行处理;方法3是通过将这个未处理的方法的所有信息全部封装,创建NSInvocation对象,再开启完整转发机制。方法3相比于方法2,它可以给多个对象做转发,来处理该对象。
以上是三者的区别。但是,最大的问题我认为还是在于,层层递进之下,性能的消耗也是在不断攀升的。所以,在不考虑具体特定需求的情况下,我们还是遵照性能最优来处理。当然,如果有特定的需求,如多重继承等等,则根据实际情况,在做判断。

未完成部分:
1.NSInvocation的使用
2.消息转发的平时使用案例

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

推荐阅读更多精彩内容