[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 impreturn 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没有, 会去父类链查找resolveInstanceMethodNSObject默认有),若是这个方法里面处理了我们查找的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) 
        ...
    

resolveClassMethodcls本来是元类,通过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,或者在转发过程中操作参数或返回值,则此方法没有用处。
能够得知这个方法是一个重定向的过程。我们可以通过实现MyPersonforwardingTargetForSelector:方法来使它成功调用它本身并未实现的方法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:方法慢速查找

objc_msgSend消息传递学习笔记 – 消息转发

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

[OC学习笔记]分类和关联对象源码解析

[OC学习笔记]分类和关联对象源码解析

[OC学习笔记]分类和关联对象源码解析