[OC学习笔记]objc_msgSend:动态方法决议和消息转发
Posted Billy Miracle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OC学习笔记]objc_msgSend:动态方法决议和消息转发相关的知识,希望对你有一定的参考价值。
一、forward_imp
在上一节,我们讲到了在寻找imp
的递归父类过程中,如果父类为nil
,那么会执行imp = forward_imp
继续寻找。下面我们通过一个例子来学习:
通常我们在只写方法声明,不写方法实现或者利用函数performSelector:@selector(XXX)
但是都没有实现方法的时候,就会报方法找不到错误:unrecognized selector sent to instance XXX
。其实这个错误来自于lookUpImpOrForward
中的forward_imp
方法:
const IMP forward_imp = (IMP)_objc_msgForward_impcache; // 汇编方法
全局搜索objc_msgForward_impcache
,可以发现这个方法在汇编里面:
/********************************************************************
*
* id _objc_msgForward(id self, SEL _cmd,...);
*
* _objc_msgForward is the externally-callable
* function returned by things like method_getImplementation().
* _objc_msgForward_impcache is the function pointer actually stored in
* method caches.
*
********************************************************************/
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward // 进入 __objc_msgForward
adrp x17, __objc_forward_handler@PAGE // 调用__objc_forward_handler方法
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17 // 返回函数指针
END_ENTRY __objc_msgForward // 结束 __objc_msgForward
__objc_msgForward_impcache
里面调用__objc_msgForward
方法进入__objc_msgForward
,我们先介绍下ADRP指令
:
- 编译时,首先会计算出当前PC到exper的偏移量#offset_to_exper
- pc的低12位清零,然后加上偏移量,给register
- 得到的地址,是含有label的4KB对齐内存区域的base地址
Page 的地址:符号扩展 21 位偏移量,将其向左移 12,并将其添加到 PC 的值中,并清除其底部 12 位,
将结果写入寄存器 Xd。这计算包含标签的4KiB对齐内存区域的基址,并设计为与提供标签地址底部12位的加载,
存储或ADD指令结合使用。这允许使用两条指令对PC的+4GiB内的任何位置进行位置依赖寻址,
前提是动态重新定位的最小粒度为4KiB(即标签地址的底部12位不受重新定位的影响)。
术语“页面”是4KiB重定位颗粒的简写,不一定与虚拟内存页面大小有关。
通俗讲为:ADRP指令可以理解成先进行PC + imm(偏移值)然后找到lable
所在的一个4KB的页,然后取得label
的基址,再进行偏移去寻址。
这里就是 adrp
指令计算出这个__objc_forward_handler@PAGE
(偏移量),之后将基址存到寄存器X17中,关键就是__objc_forward_handler
方法。
ldr: LDR R0,[R1, #8];
将存储器地址为R1+8的字数据读入寄存器R0。这里实际上将x17又去除偏移量, 给到p17中。
可看到其实关键是调用了一个__objc_forward_handler
,我们全局搜索一下可发现是一个C++方法,在objc-runtime.mm
中, 看下_objc_forward_handler
:
/***********************************************************************
* objc_setForwardHandler
**********************************************************************/
#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, cold)) 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;
可看到报错信息...unrecognized selector sent to instance XXX...
就是我们经典的报错方法, 并且其中可看到底层是不区分类方法 +与对象方法/实例方法 -的。“+”/ "-"在这里是人为拼接上的。我们之前在Clang也看过类方法在是元类调用实例方法形式存在的。调用地方需要返回再看lookUpImpOrForward
方法,如果父类链都没找到就令imp = forward_imp;
最终方法如果没有找到就会往下走return imp
即return forward_imp
,就会报那个经典方法找不到错误。
二、动态方法决议
仔细查看lookUpImpOrForward
源码,imp = forward_imp;
赋值后只是单纯break
出了循环,并不会直接return
。在return
前(循环外面)可以看到一个if
条件块:
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER))
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
这里就是进行动态方法决议的部分。据此,我们可以画出lookUpImpOrForward
慢速查找流程图:
看了流程图,相信大家都对这个流程又了一定的认识。下面就先看一下resolveMethod_locked
的代码:
/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass())
// 当前类不是元类, 即查找的是实例方法
// try [cls resolveInstanceMethod:sel]
// 实例方法的动态方法决议流程
resolveInstanceMethod(inst, sel, cls);
else
// 当前类是元类, 即查找的是类方法
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
// 类方法的动态方法决议流程
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls))
resolveInstanceMethod(inst, sel, cls);
// chances are that calling the resolver have populated the cache
// so attempt using it
// 如果有动态方法决议, 处理了, 就调用lookUpImpOrForwardTryCache重新帮你处理下
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
可以这样理解,当前方法都imp
没找到,系统给一次补救机会:动态方法决议。
- 实例方法 -> resolveInstanceMethod
- 类方法 -> resolveClassMethod
下面先看实例方法的。
(一)实例方法:resolveInstanceMethod
/***********************************************************************
* resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
// 定义一个resolveInstanceMethod 的方法编号resolve_sel
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 判断当前类, 父类等(NSObject默认有决议方法) 是否有决议方法resolveInstanceMethod,
// 根本没写就直接返回
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true)))
// Resolver not implemented.
return;
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
// 缓存结果(好或坏),以便下次不会触发解析器。
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving)
...
动态方法决议其实是调用了一个决议方法,拿实例方法举例就是resolveInstanceMethod
,如果当前imp
没有, 会去父类链查找resolveInstanceMethod
(NSObject
默认有),若是这个方法里面处理了我们查找的sel
、实现、转发等等,那么系统也算你成功并不会报错。
lookUpImpOrNilTryCache
// 定义一个resolveInstanceMethod 的方法编号resolve_sel
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 判断当前类, 父类等(NSObject默认有决议方法) 是否有决议方法resolveInstanceMethod,
// 根本没写就直接返回
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true)))
// Resolver not implemented.
return;
定义一个决议方法SEL:resolve_sel
,然后走下面if
判断,是否存在。lookUpImpOrNilTryCache
的第三个参数是:cls->ISA(true)
inline Class
objc_object::ISA(bool authenticated)
ASSERT(!isTaggedPointer());
return isa.getDecodedClass(authenticated);
inline Class
isa_t::getDecodedClass(bool authenticated)
#if SUPPORT_INDEXED_ISA//真机为1, 否则为0
// 判断是否为nonpointer, 通常已经初始化完毕的类, isa中nonpointer为nil
if (nonpointer)
return classForIndex(indexcls);
// 返回当前类
return (Class)cls;
#else
// 非真机getClass( )
return getClass(authenticated);
#endif
回到lookUpImpOrNilTryCache
:
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
可发现它是调用_lookUpImpTryCache
方法。
补救
我们看一下补救的措施,在main
里:
[me performSelector:@selector(sayHello2)];
实际上没有做sayHello2
的声明和实现,但我们实现类方法resolveInstanceMethod:
+ (BOOL)resolveInstanceMethod:(SEL)sel
if (sel == @selector(sayHello2))
//获取sayHello方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayHello));
//获取sayHello的实例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
//获取sayMethod的方法签名
const char *type = method_getTypeEncoding(sayMethod);
//将sel的实现指向
return class_addMethod(self, sel, imp, type);
return YES;
return [super resolveInstanceMethod:sel];
在执行实例方法sayHello2
时,在快速、慢速查找都没有找到的情况下,就会执行到resolveInstanceMethod
方法。我们可以看到,它正常输出了。
(二)类方法:resolveClassMethod
/***********************************************************************
* resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls))
// Resolver not implemented.
return;
Class nonmeta;
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized())
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving)
...
resolveClassMethod
中cls
本来是元类,通过getMaybeUnrealizedNonMetaClass
来获取元类对应的类对象,然后再对类对象发送消息(相当于调用类方法)。
补救:
[me sayNB];
我们调用一个没有实现的类方法,在类的实现里面实现resolveClassMethod
方法:
+ (BOOL)resolveClassMethod:(SEL)sel
if (sel == @selector(sayNB))
IMP imp = class_getMethodImplementation(objc_getMetaClass("MyPerson"), @selector(sayFuckOff));
Method classMethod = class_getInstanceMethod(objc_getMetaClass("MyPerson"), @selector(sayFuckOff));
const char *type = method_getTypeEncoding(classMethod);
return class_addMethod(objc_getMetaClass("MyPerson"), sel, imp, type);
return [super resolveClassMethod:sel];
就可以完美补救了。
三、消息转发
如果动态解析(动态决议)阶段还是没有对应的方法,那么就会来到消息转发阶段。消息转发阶段分为两部分,替换消息接收者阶段(快速转发),和完全消息转发阶段(慢速转发)。
(一)快速转发流程
实现forwardingTargetForSelector
方法,并返回替换的对象。先command + shift + 0
,来打开开发文档进行查阅:
翻译一下Discussion部分:
如果对象实现(或继承)此方法,并返回非 nil
(和非 self
)结果,则返回的对象将用作新的接收方对象,并且消息调度将恢复到该新对象。(显然,如果你从这种方法返回self
,代码就会陷入无限循环。)
如果在非 root
类中实现此方法,并且对于给定的选择器,您的类没有任何可返回的内容,则应返回调用 super
的实现的结果。
此方法使对象有机会在更昂贵的 forward
调用之前重定向发送到它的未知消息:机器接管。当您只想将消息重定向到另一个对象时,这很有用,并且可能比常规转发快一个数量级。如果转发的目标是捕获 NSinvocation
,或者在转发过程中操作参数或返回值,则此方法没有用处。
能够得知这个方法是一个重定向的过程。我们可以通过实现MyPerson
的forwardingTargetForSelector:
方法来使它成功调用它本身并未实现的方法sayBBB
:
//第二根稻草,使用快速消息转发,找其他对象来实现方法
- (id)forwardingTargetForSelector:(SEL)aSelector
if (aSelector == @selector(sayBBB))
return [PersonB alloc];
return nil;
这样就完成了消息转发,而且不像动态方法决议那样臃肿。以上就是快速转发的流程了。
(二)慢速转发流程
若是PersonB
类并无实现sayBBB
方法呢?那么就会进入到methodSignatureForSelector
方法,即慢速转发流程。还是先看一下开发文档:
翻译下Discussion部分:
此方法用于协议的实现。此方法还用于必须创建 NSinvocation
对象的情况,例如在消息转发期间。如果对象维护委托或能够处理它不直接实现的消息,则应重写此方法以返回适当的方法签名。
再看Return Value部分,可以知道这是一个返回方法签名的过程。
那么我们在类中先重写methodSignatureForSelector
方法,根据开发文档可知这个方法须要搭配NSInvocation
,以及返回适当的方法签名,即NSMethodSignature
。
//第三根稻草,使用完全消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
if (aSelector == @selector(sayBBB))
// NSMethodSignature *sig = [NSMethodSignature methodSignatureForSelector:aSelector];
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return sig;
return nil;
- (void)forwardInvocation:(NSInvocation *)anInvocation
SEL aSelector = [anInvocation selector];
if (aSelector == @selector(sayBBB))
anInvocation.selector = @selector(sayAAA);
[anInvocation invokeWithTarget:[PersonB alloc]];
else
[super forwardInvocation:anInvocation];
四、查看消息转发流程
虽然我们对消息转发流程以及有所了解了,可是咱们并无在源码中看到调用的过程,那么究竟是怎么被调用的呢?在崩溃界面使用bt
指令查看堆栈。
我们在Xcode的包里面可以找到CoreFoundation的可执行文件,我们下载工具“Hopper”的试用版帮助我们反汇编。搜索___forwarding_prep_0___
可以看到:
int ___forwarding_prep_0___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7)
r29 = &saved_fp;
var_10 = q7;
var_20 = q6;
var_30 = q5;
var_40 = q4;
var_50 = q3;
var_60 = q2;
var_70 = q1;
var_80 = q0;
var_90 = r8;
var_98 = arg7;
var_A0 = arg6;
var_A8 = arg5;
var_B0 = arg4;
var_B8 = arg3;
var_C0 = arg2;
var_C8 = arg1;
r0 = ____forwarding___(&var_D0, 0x0);
if (r0 != 0x0)
r0 = *r0;
else
r0 = objc_msgSend(var_D0, var_C8);
return r0;
双击进入____forwarding___
,下面就保留主要代码了:
void ____forwarding___(int arg0, int arg1)
r29 = &saved_fp;
r31 = r31 + 0xffffffffffffffa0 - 0x110;
r19 = &var_160;
r25 = arg1;
r20 = arg0;
r21 = *(int128_t *)arg0;
r22 = *(int128_t *)(arg0 + 0x8);
if ((r21 & 0xffffffff80000000) == 0x0) goto loc_119f20;
...
loc_119f20:
r0 = object_getClass(r21);
r23 = r0;
r24 = class_getName(r0);
if (class_respondsToSelector(r23, @selector(forwardingTargetForSelector:)) == 0x0) goto loc_119f68;//⚠️
// 先判断是否有forwardingTargetForSelector方法。
// 若是forwardingTargetForSelector方法存在,即进入快速转发流程,
// 调用forwardingTargetForSelector方法
// 否则goto loc_119f68
loc_119f4c:
r0 = [r21 forwardingTargetForSelector:r2];
if (r0 != 0x0)
asm ccmp x0, x21, #0x4, ne ;
if (CPU_FLAGS & NE) goto loc_11a200;
loc_119f68://⚠️
if (strncmp(r24, "_NSZombie_", 0xa) == 0x0) goto loc_11a2c0;
// 判断是不是僵尸对象,不是则继续
loc_119f80:
if ((class_respondsToSelector(r23, @selector(methodSignatureForSelector:)) & 0x1) == 0x0) goto loc_11a32c;//⚠️⚠️⚠️
// 如果methodSignatureForSelector:实现了继续往下走到forwardInvocation:
// 如果methodSignatureForSelector:没找到就执行了doesNotRecognizeSelector:
loc_119f98:
r0 = [r21 methodSignatureForSelector:r2];
if (r0 == 0x0) goto loc_11a398;
...
loc_11a0ec:
if (class_respondsToSelector(object_getClass(r21), @selector(forwardInvocation:)) == 0x0) goto loc_11a374;
loc_11a108:
r25 = [NSInvocation _invocationWithMethodSignature:r23 frame:r20];
[r21 forwardInvocation:r2];
r26 = 0x0;
goto loc_11a13c;
loc_11a374:
____forwarding___.cold.4(r19 + 0x0, r21);
return;
loc_11a398://⚠️⚠️
r0 = sel_getName(r22)[OC学习笔记]objc_msgSend:方法慢速查找