该文是 objc_msgSend消息传递学习笔记 – 对象方法消息传递流程 的基础上继续探究源码,请先阅读上文。

消息转发机制(message forwarding)

Objective-C 在调用对象方法的时候,是通过消息传递机制来查询且执行方法。如果想令该类能够理解并执行方法,必须以程序代码实现出对应方法。但是,在编译期间向类发送了无法解读的消息并不会报错,因为在 runtime 时期可以继续向类添加方法,所以编译器在编译时还无法确认类中是否已经实现了消息方法。

当对象接受到无法解读的消息后,就会启动消息转发机制,并且我们可以由此过程告诉对象应该如何处理位置消息。

本文的研究目标:当 Class 对象的 .h 文件中声明了成员方法,但是没有对其进行实现,来跟踪一下 runtime 的消息转发过程。于是创造一下实验场景:

同上一篇文章一样,定义一个自定义 Class DGObject ,并且声明改 Class 中拥有方法 - (void)test_no_exist ,而在 .m 文件中不给予实现。在 main.m 入口中直接调用该类对象的 - (void)test_no_exist 方法。

动态方法解析

依旧在 lookUpImpOrForward 方法中下断点,并单步调试,观察代码走向。由于方法在方法列表中无法找到,所以立即进入 method resolve 过程。

// 进入method resolve过程

if (resolver  &&  !triedResolver) {

// 释放读入锁

runtimeLock.unlockRead();

// 调用_class_resolveMethod,解析没有实现的方法

_class_resolveMethod(cls, sel, inst);

// 进行二次尝试

triedResolver = YES;

goto retry;

}

runtimeLock.unlockRead() 是释放读入锁操作,这里是指缓存读入,即缓存机制不工作从而不会有缓存结果。随后进入 _class_resolveMethod(cls, sel, inst) 方法。

void _class_resolveMethod(Class cls, SEL sel, id inst) {

// 用 isa 查看是否指向元类 Meta Class

if (! cls->isMetaClass()) {

// try [cls resolveInstanceMethod:sel]

_class_resolveInstanceMethod(cls, sel, inst);

}

else {

// try [nonMetaClass resolveClassMethod:sel]

// and [cls resolveInstanceMethod:sel]

_class_resolveClassMethod(cls, sel, inst);

if (!lookUpImpOrNil(cls, sel, inst,

NO/*initialize*/, YES/*cache*/, NO/*resolver*/))

{

_class_resolveInstanceMethod(cls, sel, inst);

}

}

}

此方法是动态方法解析的入口,会间接地发送 +resolveInstanceMethod 或 +resolveClassMethod 消息。通过对 isa 指向的判断,从而分辨出如果是对象方法,则进入 +resolveInstanceMethod 方法,如果是类方法,则进入 +resolveClassMethod 方法。

而上述代码中的 _class_resolveInstanceMethod 方法,我们从源码中看到是如此定义的:

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) {

// 首先查找是否有 resolveInstanceMethod 方法

if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,

NO/*initialize*/, YES/*cache*/, NO/*resolver*/))

{

// Resolver not implemented.

return;

}

// 构造布尔类型变量表达式,动态绑定函数

BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;

// 获得是否重新传递消息标记

bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.

// +resolveInstanceMethod adds to self a.k.a. cls

// 调用 lookUpImpOrNil 并重新启动缓存,查看是否已经添加上了选择子对应的 IMP

指针

IMP imp = lookUpImpOrNil(cls, sel, inst,

NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

// 对查询到的 IMP 进行 log 输出

if (resolved  &&  PrintResolving) {

if (imp) {

_objc_inform("RESOLVE: method %c[%s %s] "

"dynamically resolved to %p",

cls->isMetaClass() ? '+' : '-',

cls->nameForLogging(), sel_getName(sel), imp);

}

else {

// Method resolver didn't add anything?

_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"

", but no new implementation of %c[%s %s] was found",

cls->nameForLogging(), sel_getName(sel),

cls->isMetaClass() ? '+' : '-',

cls->nameForLogging(), sel_getName(sel));

}

}

}

通过 _class_resolveInstanceMethod 可以了解到,这只是通过 +resolveInstanceMethod 来查询是否开发者已经在运行时将其动态插入类中的实现函数。并且重新触发 objc_msgSend 方法。这里有一个 C 的语法值得我们去延伸学习一下,就是关于关键字 __typeof__ 的。__typeof__(var) 是 GCC 对 C 的一个扩展保留字(官方文档),这里是用来描述一个指针的类型。

https://gcc.gnu.org/onlinedocs/gcc/Typeof.html

我们发现,最终都会返回到 objc_msgSend 中。反观一下上一篇文章写的 objc_msgSend 函数,是通过汇编语言实现的。在 Let’s build objc_msgsend 这篇资料中,记录了一个关于 objc_msgSend 的伪代码。

http://t.cn/RchZ9w1

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

Class c = object_getClass(self);

IMP imp = cache_lookup(c, _cmd);

if(!imp)

imp = class_getMethodImplementation(c, _cmd);

return imp(self, _cmd, ...);

}

在缓存中无法直接击中 IMP 时,会调用 class_getMethodImplementation 方法。在 runtime 中,查看一下 class_getMethodImplementation 方法。

IMP class_getMethodImplementation(Class cls, SEL sel)

{

IMP imp;

if (!cls  ||  !sel) return nil;

// 上一篇文章的搜索入口

imp = lookUpImpOrNil(cls, sel, nil,

YES/*initialize*/, YES/*cache*/, YES/*resolver*/);

// Translate forwarding function to C-callable external version

if (!imp) {

return _objc_msgForward;

}

return imp;

}

在上一篇文中,详细介绍过了 lookUpImpOrNil 函数成功搜索的流程。而本例中与前相反,我们我发现该函数返回了一个 _objc_msgForward 的 IMP。此时,我们击中的函数是 _objc_msgForward 这个 IMP ,于是消息转发机制进入了备援接收流程。

Forwarding 备援接收

_objc_msgForward 居然可以返回,说同 IMP 一样是一个指针。在 objc-msg-x86_64.s 中发现了其汇编实现。

ENTRY    __objc_msgForward

// Non-stret version

// 调用 __objc_forward_handler

movq    __objc_forward_handler(%rip), %r11

jmp    *%r11

END_ENTRY    __objc_msgForward

发现在接收到 _objc_msgForward 指针后,会立即进入 __objc_forward_handler 方法。其源码在 objc-runtime.mm 中。

#if !__OBJC2__

// Default forward handler (nil) goes to forward:: dispatch.

void *_objc_forward_handler = nil;

void *_objc_forward_stret_handler = nil;

#else

// Default forward handler halts the process.

__attribute__((noreturn)) void

objc_defaultForwardHandler(id self, SEL sel) {

_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "

"(no message forward handler is installed)",

class_isMetaClass(object_getClass(self)) ? '+' : '-',

object_getClassName(self), sel_getName(sel), self);

}

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

在 ObjC 2.0 以前,_objc_forward_handler 是 nil ,而在最新的 runtime 中,其实现由 objc_defaultForwardHandler 完成。其源码仅仅是在 log 中记录一些相关信息,这也是 handler 的主要功能。

而抛开 runtime ,看见了关键字 __attribute__((noreturn)) 。这里简单介绍一下 GCC 中的又一扩展 attribute机制 。它用于与编译器直接交互,这是一个编译器指令(Compiler Directive),用来在函数或数据声明中设置属性,从而进一步进行优化(继续了解可以阅读 NShipster _attribute_)。而这里的 __attribute__((noreturn)) 是告诉编译器此函数不会返回给调用者,以便编译器在优化时去掉不必要的函数返回代码。

http://nshipster.com/__attribute__/

Handler 的全部工作是记录日志、触发 crash 机制。如果开发者想实现消息转发,则需要重写 _objc_forward_handler 中的实现。这时引入 objc_setForwardHandler 方法:

void objc_setForwardHandler(void *fwd, void *fwd_stret) {

_objc_forward_handler = fwd;

#if SUPPORT_STRET

_objc_forward_stret_handler = fwd_stret;

#endif

}

这是一个十分简单的动态绑定过程,让方法指针指向传入参数指针得以实现。

Core Foundation 衔接

引入 objc_setForwardHandler 方法后,会有一个疑问:如何调用它?先来看一段异常信息:

2016-08-27 08:26:08.264 debug-objc[7013:29381250] -[DGObject test_no_exist]: unrecognized selector sent to instance 0x101200310

2016-08-27 10:09:16.495 debug-objc[7013:29381250] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[DGObject test_no_exist]: unrecognized selector sent to instance 0x101200310'

*** First throw call stack:

(

0   CoreFoundation                      0x00007fff842c64f2 __exceptionPreprocess + 178

1   libobjc.A.dylib                     0x000000010002989f objc_exception_throw + 47

2   CoreFoundation                      0x00007fff843301ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205

3   CoreFoundation                      0x00007fff84236571 ___forwarding___ + 1009

4   CoreFoundation                      0x00007fff842360f8 _CF_forwarding_prep_0 + 120

5   debug-objc                          0x0000000100000e9e main + 94

6   libdyld.dylib                       0x00007fff852a95ad start + 1

7   ???                                 0x0000000000000001 0x0 + 1

)

libc++abi.dylib: terminating with uncaught exception of type NSException

这个日志场景都接触过。从调用栈上,发现了最终是通过 Core Foundation 抛出异常。在 Core Foundation 的 CFRuntime.c 无法找到 objc_setForwardHandler 方法的调用入口。综合参看 Objective-C 消息发送与转发机制原理 和 Hmmm, What’s that Selector? 两篇文章,我们发现了在 CFRuntime.c 的 __CFInitialize() 方法中,实际上是调用了 objc_setForwardHandler ,这段代码被苹果公司隐藏。

在上述调用栈中,发现了在 Core Foundation 中会调用 ___forwarding___ 。根据资料也可以了解到,在 objc_setForwardHandler 时会传入 __CF_forwarding_prep_0 和 ___forwarding_prep_1___ 两个参数,而这两个指针都会调用 ____forwarding___ 。这个函数中,也交代了消息转发的逻辑。在 Hmmm, What’s that Selector? 文章中,复原了 ____forwarding___ 的实现。

// 两个参数:前者为被转发消息的栈指针 IMP ,后者为是否返回结构体

int __forwarding__(void *frameStackPointer, int isStret) {

id receiver = *(id *)frameStackPointer;

SEL sel = *(SEL *)(frameStackPointer + 8);

const char *selName = sel_getName(sel);

Class receiverClass = object_getClass(receiver);

// 调用 forwardingTargetForSelector:

// 进入 备援接收 主要步骤

if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {

// 获得方法签名

id forwardingTarget = [receiver forwardingTargetForSelector:sel];

// 判断返回类型是否正确

if (forwardingTarget && forwarding != receiver) {

// 判断类型,是否返回值为结构体,选用不同的转发方法

if (isStret == 1) {

int ret;

objc_msgSend_stret(&ret,forwardingTarget, sel, ...);

return ret;

}

return objc_msgSend(forwardingTarget, sel, ...);

}

}

// 僵尸对象

const char *className = class_getName(receiverClass);

const char *zombiePrefix = "_NSZombie_";

size_t prefixLen = strlen(zombiePrefix); // 0xa

if (strncmp(className, zombiePrefix, prefixLen) == 0) {

CFLog(kCFLogLevelError,

@"*** -[%s %s]: message sent to deallocated instance %p",

className + prefixLen,

selName,

receiver);

}

// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation

// 进入消息转发系统

if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {

NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];

// 判断返回类型是否正确

if (methodSignature) {

BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;

if (signatureIsStret != isStret) {

CFLog(kCFLogLevelWarning ,

@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.  Signature thinks it does%s return a struct, and compiler thinks it does%s.",

selName,

signatureIsStret ? "" : not,

isStret ? "" : not);

}

if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {

// 传入消息的全部细节信息

NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

[receiver forwardInvocation:invocation];

void *returnValue = NULL;

[invocation getReturnValue:&value];

return returnValue;

} else {

CFLog(kCFLogLevelWarning ,

@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",

receiver,

className);

return 0;

}

}

}

SEL *registeredSel = sel_getUid(selName);

// selector 是否已经在 Runtime 注册过

if (sel != registeredSel) {

CFLog(kCFLogLevelWarning ,

@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",

sel,

selName,

registeredSel);

}

// doesNotRecognizeSelector,主动抛出异常

// 也就是前文我们看到的

// 表明选择子未能得到处理

else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {

[receiver doesNotRecognizeSelector:sel];

}

else {

CFLog(kCFLogLevelWarning ,

@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",

receiver,

className);

}

// The point of no return.

kill(getpid(), 9);

}

Message-Dispatch System 消息派发系统

在大概了解过 Message-Dispatch System 的源码后,来简单的说明一下。由于在前两步中,我们无法找到那条消息的实现。则创建一个 NSInvocation 对象,并将消息全部属性记录下来。 NSInvocation 对象包括了选择子、target 以及其他参数。

随后,调用 forwardInvocation:(NSInvocation *)invocation 方法,其中的实现仅仅是改变了 target 指向,使消息保证能够调用。倘若发现本类无法处理,则继续想父类进行查找。直至 NSObject ,如果找到根类仍旧无法找到,则会调用 doesNotRecognizeSelector: ,以抛出异常。此异常表明选择子最终未能得到处理。

而对于 doesNotRecognizeSelector: 内部是如何实现,如何捕获异常。或者说 override 改方法后做自定义处理,等笔者实践后继续记录学习笔记。

对于消息转发的总结梳理

在 Core Foundation 的消息派发流程中,由于源码被隐藏,所以笔者无法亲自测试代码。倘若以后学习了逆向,可以再去探讨一下这里面发生的过程。

对于这篇文章记录的消息转发流程,大致如下图所示:

objc_msgSend消息传递学习笔记 – 消息转发的更多相关文章

  1. objc_msgSend消息传递学习笔记 – 对象方法消息传递流程

    在Effective Objective-C 2.0 – 52 Specific Ways to Improve Your iOS and OS X Programs一书中,tip 11主要讲述了Ob ...

  2. Storm学习笔记 - 消息容错机制

    Storm学习笔记 - 消息容错机制 文章来自「随笔」 http://jsynk.cn/blog/articles/153.html 1. Storm消息容错机制概念 一个提供了可靠的处理机制的spo ...

  3. 深入浅出MFC学习笔记 消息

    本章将会对MFC的消息映射和 命令传递机制做深入探讨. MFC规定了消息传递的路线,消息会按照这个路线传递下去,找不到归宿的话就交给DefWindowProc. 在产生窗口之前,如果我们在创建窗口时指 ...

  4. VBA 学习笔记 - 消息框

    学习资料:https://www.yiibai.com/vba/vba_macro_comments.html 注释 单引号或 REM 开头 丸子:多行注释咋办? 消息框(MsgBox) 函数功能:显 ...

  5. linux 进程学习笔记-消息队列messagequeue

    可以想象,如果两个进程都可以访问同一个队列:其中一个进程(sender)向其中写入结构化数据,另外一个进程(receiver)再从其中把结构化的数据读取出来.那么这两个进程就是在利用这个队列进行通信了 ...

  6. Objective-C RunTime 学习笔记 之 消息转发流程

    1) 当向某个对象发送消息时,先从cache(cache_t)中查找方法对象(method_t),如果找到则进行回调:否则通过查找对象的类(元类)定义中方法列表,一直追溯到NSObject, 如果找到 ...

  7. Android学习笔记-事件处理之Handler消息传递机制

    内容摘要:Android Handler消息传递机制的学习总结.问题记录 Handler消息传递机制的目的: 1.实现线程间通信(如:Android平台只允许主线程(UI线程)修改Activity里的 ...

  8. kafka学习笔记(一)消息队列和kafka入门

    概述 学习和使用kafka不知不觉已经将近5年了,觉得应该总结整理一下之前的知识更好,所以决定写一系列kafka学习笔记,在总结的基础上希望自己的知识更上一层楼.写的不对的地方请大家不吝指正,感激万分 ...

  9. ucos实时操作系统学习笔记——任务间通信(消息)

    ucos另一种任务间通信的机制是消息(mbox),个人感觉是它是queue中只有一个信息的特殊情况,从代码中可以很清楚的看到,因为之前有关于queue的学习笔记,所以一并讲一下mbox.为什么有了qu ...

随机推荐

  1. 2016年CCF第七次测试 俄罗斯方块

    //2016年CCF第七次测试 俄罗斯方块 // 这道小模拟题还是不错 // 思路:处理出输入矩阵中含1格子的行数和列数 // 再判是否有一个格子碰到底部,否则整体再往下移动一步,如果有一个格子不能移 ...

  2. RT-thread内核之进程间通信

    这里面见到的同步和互斥的概念非常清晰,转载自: http://www.cnblogs.com/King-Gentleman/p/4311582.html 一.进程间通信机制 rt-thread操作系统 ...

  3. 通用表表达式(Common Table Expression)

    问题:编写由基本的 SELECT/FROM/WHERE 类型的语句派生而来的复杂 SQL 语句. 方案1:编写在From子句内使用派生表(内联视图)的T-SQL查询语句. 方案2:使用视图 方案3:使 ...

  4. linux查看端口信息以及关闭进程

     lsof -i:6633 查看端口6633的使用情况 kill  (+PID数值),结束进程

  5. STS中搭建SpringMVC工程

    1 环境说明 首次接触Spring,面对这么一个优秀的框架,先从环境搞起,再慢慢学.开发环境选择Spring Tool Suite,得专业点不是?Maven选2.2.1,JDK还是1.6,Tomcat ...

  6. [转]SQL中char、varchar、nvarchar的区别

    char    char是定长的,也就是当你输入的字符小于你指定的数目时,char(8),你输入的字符小于8时,它会再后面补空值.当你输入的字符大于指定的数时,它会截取超出的字符.   nvarcha ...

  7. ocp 1Z0-043 1-60题解析

    1.You observe that a database performance has degraded overa period of time. While investigating the ...

  8. Spring JdbcTemplate Querying examples

    Here are few examples to show you how to use JdbcTemplate query() methods to query or extract data f ...

  9. 2015年必火的五个Html5移动开发工具推荐

    NO.1   DCloudHBuilder:基于HTML5开发工具 中文官网:http://www.dcloud.io/ DCloudHBuilder:基于HTML5开发工具是当前最快的HTML开发工 ...

  10. C/C++ 不带参数的回调函数 与 带参数的回调函数 函数指针数组 例子

    先来不带参数的回调函数例子 #include <iostream> #include <windows.h> void printFunc() { std::cout<& ...