iOS的OC的方法的决议与消息转发原理

1,853 阅读10分钟

前言

笔者整理了一系列有关OC的底层文章,希望可以帮助到你。

1.iOS的OC对象创建的alloc原理

2.iOS的OC对象的内存对齐

3.iOS的OC的isa的底层原理

4.iOS的OC源码分析之类的结构分析

5.iOS的OC的方法缓存的源码分析

6.iOS的OC的方法的查找原理

OC的方法的查找是通过消息的发送来查找函数的IMP,首先通过objc_msgSend来进行快速查找(cache_t),如果快速找不到,就需要进行方法的慢速查找,具体可以了解iOS的OC的方法的查找原理这篇文章。但是,如果通过快速和慢速的查找都找不到的话,就会直接报错。是不是说如果找不到就没有办法走其他的操作了呢?并不是的,接下来这边文章就是介绍方法的决议和消息转发原理。为了接下来的内容介绍定义一个TestObject类。

1.方法的决议

实现下面的代码,其中testErrorMthod方法是没有在TestObject类声明和实现的,直接运行是报错的。

TestObject *testObject = [[TestObject alloc] init];
[testObject performSelector:@selector(testErrorMthod)];

==========运行结果=================

LGTest[1639:40195] -[TestObject testErrorMthod]: unrecognized selector sent to instance 0x1010c3600
LGTest[1639:40195] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TestObject testErrorMthod]: unrecognized selector sent to instance 0x1010c3600'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff3c7438ab __exceptionPreprocess + 250
	1   libobjc.A.dylib                     0x000000010038dfea objc_exception_throw + 42
	2   CoreFoundation                      0x00007fff3c7c2b61 -[NSObject(NSObject) __retain_OA] + 0
	3   CoreFoundation                      0x00007fff3c6a7adf ___forwarding___ + 1427
	4   CoreFoundation                      0x00007fff3c6a74b8 _CF_forwarding_prep_0 + 120
	5   libobjc.A.dylib                     0x00000001003ccf26 -[NSObject performSelector:] + 70
	6   LGTest                              0x0000000100001afd main + 93
	7   libdyld.dylib                       0x00007fff73d6b7fd start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

在之前介绍方法的慢速查找流程中的lookUpImpOrForward函数中,有这段源码,在方法查找不到的时候,会执行到里面去。

 // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

1.1 _class_resolveMethod

下面是_class_resolveMethod的源码,其中cls是类,sel方法的编号,inst是实例对象。

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    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);
        }
    }
}

这段源码对传进来的cls判断是否是元类,通过之前的文章可以知道类方法是存在元类中的。所以如果传进来的cls是类,就直接执行_class_resolveInstanceMethod函数,如果是元类执行_class_resolveClassMethod函数。

1.2 _class_resolveInstanceMethod

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    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
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

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

其中lookUpImpOrNil函数对类方法SEL_resolveInstanceMethod查找是否有存在,发现SEL_resolveInstanceMethod的实现是resolveInstanceMethod方法,这个是在NSObject类里面有实现的,所以这个是返回true的。

然后接着执行,下面这段代码通过objc_msgSend查找当前的类是否执行resolveInstanceMethod方法。

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
IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

通过前面的文章,可以知道如果当前的类没有实现resolveInstanceMethod类方法就会查找父类是否实现,因为在NSObject类中是默认实现返回NO的,如果当前的类有实现resolveInstanceMethod类方法就会执行方法里的内容。并且在实现的resolveInstanceMethod类方法中对没有找到的sel重新赋值一个IMP,并且会将实现的IMP缓存起来,通过lookUpImpOrNil来重新查找一次,下面是在TestObject类的实现代码。

-(void)testOk{
    NSLog(@"%p===testOk",__func__);
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"===执行resolveInstanceMethod===%s===%@",__func__,NSStringFromSelector(sel));
    if(sel == @selector(testErrorMthod)){
        Method okMethod = class_getInstanceMethod(self, @selector(testOk));
        IMP okImp = method_getImplementation(okMethod);
        const char *type =  method_getTypeEncoding(okMethod);
        return class_addMethod(self, sel, okImp, type);
    }
    return [super resolveInstanceMethod:sel];
}


======执行的结果=======
LGTest[2769:92089] ===执行resolveInstanceMethod===+[TestObject resolveInstanceMethod:]===testErrorMthod
LGTest[2769:92089] 0x100001f2a===testOk
Program ended with exit code: 0

此时还执行一次lookUpImpOrNil函数在缓存中查找。所以在TestObject类中定义了这个方法并且为testErrorMthod赋值了新的IMP

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

上面的是对实例方法的动态决议,其实对类方法的也是差不多的,如果是类方法此时会执行如下源码

        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }

1.3 _class_resolveClassMethod

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_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 = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

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

通过源码可以知道是与_class_resolveInstanceMethod源码是差不多的,主要区别是,这个源码的SEL_resolveClassMethod是要实现resolveClassMethod这个类方法,需要在TestObject类中实现类方法resolveClassMethod并在这个方法里面实现逻辑。但是为什么执行完_class_resolveClassMethod函数之后还会再做一次lookUpImpOrNil函数的判断呢?因为如果在_class_resolveClassMethod是没有做处理的,由于元类的查找方法查找流程是会往根元类查找最终会找到NSObject这个类,所以如果在根元类都找不到的情况下会找到NSObject类的方法里面。而NSObject类是有默认实现了这两个类方法的并且默认返回NO

2. 消息转发

如果对不存在的方法的查找,没有实现上面的方法决议,此时会在lookUpImpOrForward函数中执行

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

其中_objc_msgForward_impcache就会执行到汇编中的内容,然后执行__objc_msgForward,最终会执行到_objc_forward_handler函数中,最终是会报错的。


	STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

	
	ENTRY __objc_msgForward

	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
	TailCallFunctionPointer x17
	
	END_ENTRY __objc_msgForward
	
// 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;

这些流程就是在之前文章的方法的查找原理有介绍。是不是就是说消息的转发流程就跟宗不了呢?并不是的。在lookUpImpOrForward函数中有一个可以打印log的方法log_and_fill_cache中的logMessageSend方法里面有介绍可以根据objcMsgLogEnabled属性来控制打印log

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}

为了可以看到消息转发的过程中实现了那些方法,可以在mac的项目中实现

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        TestObject *test = [TestObject alloc] ;
        instrumentObjcMessageSends(true);
        [test performSelector:@selector(testErrorMthod)];
        instrumentObjcMessageSends(false);

    }
    return 0;
}

就可以在路径:/tmp/msgSends-找到生成的msgSends文件

这个msgSends-7265文件的内容

这个打印log是要在mac的项目下才可以,如果在其他的项目下是会报objc[6984]: lock 0x100cbf0c0 (runtimeLock) acquired before 0x100cbf040 (objcMsgLogLock) with no defined lock order这种错误。

从打印出来的方法可以知道,在消息的转发的过程中执行的过程是resolveInstanceMethod-->forwardingTargetForSelector-->methodSignatureForSelector-->resolveInstanceMethod-->doesNotRecognizeSelector。所以如果不执行resolveInstanceMethod方法决议,会执行forwardingTargetForSelector方法。

2.1 消息快速转发

objc的源码中可以找到在NSObject.mm文件中有定义和实现forwardingTargetForSelector的实例方法和类方法。

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

在官方的文档中有对forwardingTargetForSelector方法的介绍

大概的意思是,forwardingTargetForSelector主要是返回一个不是自身(如果是self会进入死循坏)的对象去处理sel这个当前类无法处理的消息,其他的情况可以调用super方法。如果处理不了,会转到效率低下的forwardInvocation。在效率方面,forwardingTargetForSelector领先forwardInvocation一个数量级,因此,如果可以的话最好避免使用后者来做消息转发。下面在TestObject类中添加多一个TestForwardObject类,并且在TestObject类中实现forwardingTargetForSelector方法。

@interface TestForwardObject : NSObject
@end

@implementation TestForwardObject

-(void)testErrorMthod{
    NSLog(@"TestForwardObject的testErrorMthod方法%p",__func__);
}
@end

//在TestObject类中实现的方法
-(id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"方法名字:%@",NSStringFromSelector(aSelector));
    if(aSelector == @selector(testErrorMthod)){
        return [TestForwardObject alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

//=========运行的结果========
LGTest[1308:29082] 方法名字:testErrorMthod
LGTest[1308:29082] TestForwardObjecttestErrorMthod方法0x100001eb4

从中可以看到消息的转发到TestForwardObjecttestErrorMthod方法执行了。但是需要注意的是转发到其他的类执行的方法必须要和被调用的方法相同方法签名的方法(方法名、参数列表、返回值类型都必须一致)。否则的话,还是报错的。

2.2消息慢速转发

如果在消息转发的慢速流程中不做处理,此时会执行到消息转发的慢速流程中,需要分别执行两个方法分别是methodSignatureForSelectorforwardInvocation

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"方法名字:%@",NSStringFromSelector(aSelector));
    if(aSelector == @selector(testErrorMthod)){
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"执行forwardInvocation:%s",__func__);
}

//======运行结果==========
LGTest[1222:21266] 方法名字:testErrorMthod
LGTest[1222:21266] 执行forwardInvocation-[TestObject forwardInvocation:]

在这个流程中methodSignatureForSelector是返回的方法的签名,可以参考 苹果官方类型编码。可以发现在forwardInvocation方法中就算不做处理也不会奔溃,因为每个方法其实就是一个事务,不做处理就会失效,在forwardInvocation中做处理的话,可以如下:

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"执行forwardInvocation:%s",__func__);
    SEL inVocationSeletor = [anInvocation selector];
    if([[TestForwardObject alloc] respondsToSelector:inVocationSeletor]){
        [anInvocation invokeWithTarget:[TestForwardObject alloc]];
    }else{
        [super forwardInvocation:anInvocation];
    }
}

//=====运行结果===========
LGTest[1465:30634] 方法名字:testErrorMthod
LGTest[1465:30634] 执行forwardInvocation:-[TestObject forwardInvocation:]
LGTest[1465:30634] TestForwardObject的testErrorMthod方法-[TestForwardObject testErrorMthod]

3.最后

OC方法调用是通过objc_msgSend先通过cache_t的快速查找,如果找不到就要进行慢速查找。如果都查找不到方法,就会进入方法的决议消息转发流程。如果查找的类有实现resolveInstanceMethodresolveClassMethod方法对需要查找的方法做处理就完成,否则就进入消息转发流程。消息转发的流程中先进入消息快速转发流程,需要实现forwardingTargetForSelector方法。否则进入消息慢速转发流程,需要实现methodSignatureForSelectorforwardInvocation方法。如果都没有,此时程序只能报错了。最后附上消息转发的流程图