iOS-15.方法查找流程之动态方法决议&消息转发

411 阅读8分钟

ios底层文章汇总

1 引入动态方法决议

iOS方法调用的时候,底层实际是消息发送objc_msgSend,这就需要找到具体的方法sel对应的函数指针imp,查找imp的流程初略可划分为以下几个步骤:

底层会经过快速查找 (详细见:iOS-13.objc_msgSend快速查找流程);

当快速查找未找到对应的imp,将会进入慢速查找(详细见:iOS-14.objc_msgSend方法慢速查找流程分析);

当慢速查找也未找到对应的imp,将进行动态方法决议;

动态方法决议后进行慢速查找(慢速查找后不会再次进入动态方法决议),如果仍然没有找imp,则进行消息转发;

2 方法未实现报错原理

2.1 定义LGPerson类,声明一个类方法和一个对象方法,但均不进行实现


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject
- (void)say666;
+ (void)sayNB;
@end

NS_ASSUME_NONNULL_END

@implementation LGPerson

@end

2.1 对象方法调用报错

2.2 类方法调用报错

2.3 报错流程

  • 方法的调用 到底层解析为消息的发送,底层根据消息的接受对象和消息方法sel找到对应的函数指针imp

  • 函数指针imp的查找经过快速查找慢速查找动态方法决议快速消息转发都没有找到对应的imp时,会查找消息接收者中是否实现-methodSignatureForSelector:方法,并成功返回一个NSMethodSignature对象,如果未实现,程序在imp查找中返回_objc_msgForward_impcache

  • __objc_msgForward_impcache具体实现和__objc_msgForward具体实现

STATIC_ENTRY __objc_msgForward_impcache

	//跳转到__objc_msgForward
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

	
	ENTRY __objc_msgForward
    //__objc_forward_handler
    //定义在objc-runtime.mm  void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
	TailCallFunctionPointer x17
	
	END_ENTRY __objc_msgForward
  • objc_defaultForwardHandler的具体实现
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);
}

3 动态方法决议resolveMethod_locked

3.1 resolveMethod_locked函数实现源码

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 { // 类方法 存储在元类中
        resolveClassMethod(inst, sel, cls);//查找类方法
        if (!lookUpImpOrNil(inst, sel, cls)) {  //重新进入lookUpImpOrNil中查找imp
            resolveInstanceMethod(inst, sel, cls);//在底层所有方法都是对象方法,类方法是元类的对象方法,所以仍然需要查找一次对象方法
        }
    }

    //  动态方法决议中可能将其实现指向其他方法,重新走慢速查找流程
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

3.2 resolveMethod_locked执行逻辑

  • 在底层,方法的调用全部解释为消息发送
  • OC接口层存在对象方法和类方法的区分,但底层只有一种消息发送,,所以底层将对象方法和类方法都统一为对象方法,只是存储位置和归属不同,类的对象方法存储在类结构中类的类方法存储在元类中
  • 在查找对应的imp是需要判断cls是否是元类,分别查找
  • cls不是元类,则调用对象方法查找resolveInstanceMethod
  • cls是元类,则调用类方法查找resolveClassMethod
  • cls是元类,调用类方法查找resolveClassMethod后,如果未找到imp,仍会调用元类的+resolveInstanceMethod方法查找,
  • 动态方法决议后,可能将sel的实现指向其他方法,需要重新走慢速查找流程

3.3 对象方法查找resolveInstanceMethod

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // 对象方法,查找类中是否实现+ resolveInstanceMethod:方法
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // resolveInstanceMethod not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //调用+resolveInstanceMethod:方法
    bool resolved = msg(cls, resolve_sel, sel);

   
    /*
    进入动态方法决议需要behavior条件判断
    if (slowpath(behavior & LOOKUP_RESOLVER)) {

        behavior ^= LOOKUP_RESOLVER;

        return resolveMethod_locked(inst, sel, cls, behavior);

    }
    */
     //给一次机会,在resolveInstanceMethod方法中添加imp,再进行一次lookUpImpOrNil查找,但此次慢速查找不一定会再次进入动态方法决议
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    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));
        }
    }
}

3.4 类方法查找resolveClassMethod

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    //类方法 查找类中是否实现 +resolveClassMethod:
    if (!lookUpImpOrNil(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;
    //调用+resolveClassMethod:
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    // 给一次机会,在resolveClassMethod方法中添加imp,再进行一次lookUpImpOrNil查找
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    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 resolveClassMethod:%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));
        }
    }
}

4 动态方法决议解决方法未实现

4.1 在LGPerson中定义并实现两个方法

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject

- (void)say666;
- (void)sayMaster;
+ (void)sayNB;
+ (void)lgClassMethod;
@end

NS_ASSUME_NONNULL_END

@implementation LGPerson

- (void)sayMaster{
    NSLog(@"%s",__func__);
}


+ (void)lgClassMethod{
    NSLog(@"%s",__func__);
}
@end

4.2 调用对象方法报错

LGPerson *person = [LGPerson alloc];
[person say666];

在LGPerson.m中添加方法+ (BOOL)resolveInstanceMethod:(SEL)sel,在方法中往类的对象方法列表中添加添加imp,并绑定@selector(say666)

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"%s - %@ 来了",__func__,NSStringFromSelector(sel));
    
    if (sel == @selector(say666)) {
        IMP imp           = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type  = method_getTypeEncoding(sayMMethod);
        //对象方法,存储在类中
        return class_addMethod(self, sel, imp, type);
    }

    return [super resolveInstanceMethod:sel];
}

4.3 调用类方法报错

[LGPerson sayNB];

在LGPerson.m中+ (BOOL)resolveClassMethod:(SEL)sel,在方法中往元类的对象方法列表中添加imp,并绑定@selector(sayNB)

+ (BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"%@ 来了",NSStringFromSelector(sel));
    if (sel == @selector(sayNB)) {

        IMP imp           = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type  = method_getTypeEncoding(sayMMethod);
        //类方法存储在元类中
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return [super resolveClassMethod:sel];
}

4.4 在NSObjcet中拦截所有方法未实现的动态决议

对象方法动态决议 class的对象方法->【父类的对象方法->父类的父类的对象方法->...->】->NSObject对象方法

类方法动态决议

class的类方法->【父类的类方法->父类的父类的类方法->...->】->NSObject类方法->NSObject对象方法

由此可知,当没有找到方法的的实现时,都会找NSObject的对象方法。所有我们可以NSObject添加一个分类实现+resolveInstanceMethod:方法,拦截所有方法未实现的动态决议,统一处理

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, imp, type);
    }else if (sel == @selector(sayNB)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}

5 两次进行动态方法决议

如下图,如果在resolveInstanceMethod方法中没有实现sel的imp的重新绑定,将继续会方法未实现的错误,而且会调用两次resolveInstanceMethod,也就会有两次进行动态方法决议

在resolveInstanceMethod的消息发送之前断点打印堆栈信息

第一次say666方法调用resolveInstanceMethod

是按照本文上述的逻辑,在快速查找和慢速查找都没有找到imp后进行动态方法决议

第二次say666方法调用resolveInstanceMethod

是在CoreFoundation内部系统调用,在慢速消息转发methodSignatureForSelector之后,再次进行了动态方法决议

6 消息转发

动态方法决议之后也没有找到imp将会进行消息转发,开源源码中没有找到相关内容,可以通过以下两种方式探索消息转发的流程:

 1) instrumentObjcMessageSends 打印发送消息的日志

 2) 通过hopper/IDA反编译

6.1 instrumentObjcMessageSends

通过lookUpImpOrForward 中调用 log_and_fill_cache ,进入logMessageSend,

logMessageSend源码下方找到instrumentObjcMessageSends的源码实现,

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char	buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}

需要打开 objcMsgLogEnabled 开关,才能将消息调用的日志写入

  • 在main中调用: 打开 objcMsgLogEnabled 开关 --- instrumentObjcMessageSends(YES);

  • 在main中通过extern 声明instrumentObjcMessageSends方法

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}

找到消息发送的日志函数,可知日志存放路径为/tmp/msgSend-%d,找到/tmp/路径,查看文件名前缀为msgSend的文件

6.2 通过hopper/IDA反编译

当函数为实现的崩溃堆栈可知程序进入了CoreFoundation中 面对未开源的系统库,可以采用反编译工具分析

我们通过查看image list 找到CoreFoundation的路径

CoreFoundation可执行文件拖拽到Hopper中,选择x86(64 bits)

全局搜索____forwarding___找到其定义处,查看伪代码

在___forwarding___会判断forwardingTargetForSelector函数:是否实现,如果有实现,则进入forwardingTargetForSelector:快速消息转发,如果未实现则跳转到loc_64a67进入慢速消息转发

6.4 forwardingTargetForSelector:快速消息转发

LGPerson.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject
- (void)sayHello;
@end

NS_ASSUME_NONNULL_END

LGPerson.m

#import "LGPerson.h"

#import "LGStudent.h"


@implementation LGPerson
// 1: 快速转发

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));

    // runtime + aSelector + addMethod + imp
//    return [super forwardingTargetForSelector:aSelector];
  
  return [LGStudent alloc];
}
@end

LGStudent.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGStudent : NSObject
- (void)sayHello;

@end

NS_ASSUME_NONNULL_END


LGStudent.m

#import "LGStudent.h"

@implementation LGStudent
- (void)sayHello{
    NSLog(@"%s",__func__);
}

@end

方法调用

image.png

6.5 methodSignatureForSelector:+forwardInvocation:慢速消息转发

跳转到loc_64a67,后会先判断类是否实现methodSignatureForSelector:方法 如果methodSignatureForSelector:方法未实现,则跳转至loc_64dd7输出报错

如果methodSignatureForSelector:方法有实现,则进一步判断forwardInvocation:是否实现

forwardInvocation:未实现 则跳转loc_64ec2,后返回nil报unrecognized selector错误

forwardInvocation:有实现,则调用forwardInvocation:实现消息的慢速转发

image.png

NSInvocation事务类

@interface NSInvocation : NSObject

+ (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

7 消息转发注意事项

如果类中未实现methodSignatureForSelector:,在慢速查找的循环查找imp中可知会遍历父类,并在父类中进行消息转发,最终会找到基类NSObjec,通过Hopper查看NSObject的methodSignatureForSelector:实现 调用___methodDescriptionForSelector-->调用class_getInstanceMethod-->进入OC底层class_getInstanceMethod函数-->lookUpImpOrForword-->再次进行动态方法决议

8 总结

消息发送objc_msgSend,由sel找到对应的函数指针imp,查找imp的流程初略可划分为以下几个步骤:

  1. (详细见:iOS-objc_msgSend快速查找流程);

  2. 当快速查找未找到对应的imp,将会进入慢速查找(详细见:iOS- objc_msgSend方法慢速查找流程分析);

  3. 当慢速查找也未找到对应的imp,将进行动态方法决议;

  4. 当动态方法决议仍然没有找imp,则进行消息转发;

  5. 消息转发可分为快速的消息转发和慢速的消息转发,先进行快速的消息转发,当返回imp=nil进入慢速消息转发