每日一问10——runtime消息转发

终于要说到重点了,objective-c的这种有趣的语法被苹果称为“发消息”。与其他面向对象语言(C++/Java)的“方法调用”不同,objc的消息机制是由运行时实现、非常灵活动态。

消息机制

1.为什么叫发消息

先来看一段例子:

[receiver message];

这一句的含义是:向receiver发送名为message的消息。

clang -rewrite-objc MyClass.m

执行上面的命令,将这一句重写为C代码,是这样的:

((void (*)(id, SEL))(void *)objc_msgSend)((id)receiver, sel_registerName("message"));

去掉那些强制转换,最终[receiver message]会由编译器转化为以下的纯C调用。

objc_msgSend(receiver, @selector(message));

所以说,objc发送消息,最终大都会转换为objc_msgSend的方法调用

看一下objc_msgSend的声明

id objc_msgSend(id self, SEL _cmd, ...)

发现这个函数是一个不定参的函数,但有2个确定的参数,一个是id类型的receiver对象,一个是SEL类型的方法选择器_cmd。于是我们可以简单理解,objective-c中调用方法其实就是向指定对象发送一个调用方法的消息。

2.基本的数据结构

首先 runtime定义了如下的数据类型:

typedef struct objc_class *Class;
typedef struct objc_object *id;
struct objc_object {
    Class isa;
};
struct objc_class {
    Class isa;
}
 
/// 不透明结构体, selector
typedef struct objc_selector *SEL;
 
/// 函数指针, 用于表示对象方法的实现
typedef id (*IMP)(id, SEL, ...);

根据之前文章的介绍,我们已经知道了id代表对象,Class代表对象的类,都可以通过指向首地址的isa指针找到。

SEL

SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:

typedef struct objc_selector *SEL;

方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。如下:

SEL sel1 = @selector(method1);
NSLog(@"sel : %p", sel1);

打印结果sel : 0x100002d72
两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。

所以运行时维护着一张SEL的表,将相同字符串的方法名映射到唯一一个SEL。 通过sel_registerName(char *name)方法,可以查找到这张表中方法名对应的SEL。
本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法)

IMP

IMP是一个函数指针。objc中的方法最终会被转换成纯C的函数,IMP就是为了表示这些函数的地址。

id (*IMP)(id, SEL, ...)

第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。

Method

还记得之前提到的,在objct_class结构体中有struct objc_method_list **methodLists 的结构体,里面存放的是这个类中所有的方法。
于是我查阅到具体的一个方法结构体

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}  

我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。

3.方法调用流程

先看一下我从YY大神博客扒下来的汇编伪代码

id objc_msgSend(id self, SEL op, ...) {
    if (!self) return nil;
    IMP imp = class_getMethodImplementation(self->isa, SEL op);
    imp(self, op, ...); //调用这个函数,伪代码...
}
 
//查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
    if (!cls || !sel) return nil;
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) return _objc_msgForward; //这个是用于消息转发的
    return imp;
}
 
IMP lookUpImpOrNil(Class cls, SEL sel) {
    if (!cls->initialize()) {
        _class_initialize(cls);
    }
 
    Class curClass = cls;
    IMP imp = nil;
    do { //先查缓存,缓存没有时重建,仍旧没有则向父类查询
        if (!curClass) break;
        if (!curClass->cache) fill_cache(cls, curClass);
        imp = cache_getImp(curClass, sel);
        if (imp) break;
    } while (curClass = curClass->superclass);
 
    return imp;
}

首先在Class中的缓存查找imp(没缓存则初始化缓存),如果没找到,则向父类的Class查找。如果一直查找到根类仍旧没有实现,则用_objc_msgForward函数指针代替imp。最后,执行这个imp。

messaging1.gif

这个流程就和我们之前讨论过的结构体objc_class联系上了。

Class super_class            //指向父类的结构体                            
struct objc_method_list **methodLists         //方法method列表            
struct objc_cache *cache      //方法缓存cache

看到这里,我们可以基本明白objective-c中调用方法到底是怎么实现的了。也知道了所谓的“发消息”到底是怎样一件事情。

我们知道,调用一个未实现的方法时,正常情况会发生崩溃并报出'-[TestObject xxx]: unrecognized selector sent to instance。即没有找到某个方法的实现。在上面我们提到了,如果一直没有查找到那个方法的实现,则会调用_objc_msgForward。接下来我们就要说一下后续的操作。

消息转发

当我们发送一个错误的消息时

Test *test = [Test new];
[test performSelector(@selector(xxx))];

看一下具体的方法调用顺序

+ Test NSObject initialize
+ Test NSObject new
+ Test NSObject alloc
+ Test NSObject allocWithZone:
- Test NSObject init
- Test NSObject performSelector:
+ Test NSObject resolveInstanceMethod:
- Test NSObject forwardingTargetForSelector:
- Test NSObject methodSignatureForSelector:
- Test NSObject class
- Test NSObject doesNotRecognizeSelector:

可以看到,当NSObject抛出doesNotRecognizeSelector:时,程序就崩溃了。所以我们必须在这之前对此次消息进行处理。

1.动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。

- (void)viewDidLoad {
    [super viewDidLoad];
    TestObject *test = [TestObject new];
    [test test];
}

@implementation TestObject
void functionForTest(id self, SEL _cmd) {
    NSLog(@"%@, %p", self, _cmd);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selString = NSStringFromSelector(sel);
    if([selString isEqualToString:@"test"]) {
        class_addMethod(self.class, @selector(test), (IMP)functionForTest, "@:");
    }
    return [super resolveInstanceMethod:sel];
}
@end
2.备用接收者

如果在上一步无法处理消息,则Runtime会继续调以下方法:

- (id)forwardingTargetForSelector:(SEL)aSelector

如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

//备用接受对象
@implementation helpObject
- (void)test {
    NSLog(@"help test do");
}
@end

@implementation TestObject
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selString = NSStringFromSelector(aSelector);
    if([selString isEqualToString:@"test"]) {
        helpObject *help = [helpObject new];
        return help;
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end
3.完整的消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。

然后就是我们的最后一道关卡forwardInvocation:
NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。

从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。

- (void)forwardInvocation:(NSInvocation *)anInvocation

运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。

  • forwardInvocation:方法的实现有两个任务:

  • 定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。

使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

上面两步只能让这个消息变为正确,而一些复杂的操作可以在这里处理,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。

@implementation TestObject {
    helpObject *_help;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        _help = [helpObject new];
    }
    return self;
}

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

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:_help];
}

小结

通过本章,我们知道了runtime消息机制与消息转发究竟是什么样的。通过消息,我们可以给程序添加许多动态行为,让我们的程序更加灵活。

上面都太官方了,主要学习的东西我觉得是objective-c发消息的具体流程,理解方法调用的原理和系统相关的处理,最后才是利用消息转发实现一些黑魔法。

相关文章

Objective-C 中的消息与消息转发
Objective-C Runtime 运行时之三:方法与消息
消息转发示例

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,162评论 0 7
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,529评论 33 466
  • 消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 ...
    lylaut阅读 1,809评论 2 3
  • 继上Runtime梳理(四) 通过前面的学习,我们了解到Objective-C的动态特性:Objective-C不...
    小名一峰阅读 737评论 0 3