ios 底层 方法调用 消息转发

693 阅读4分钟

前言

在前面我们学到了,苹果为了容错管理,提供消息动态实现,如果动态还没有实现呢,是不是就直接崩溃了?答案是否,苹果又给我们提供了一种机制 - 消息转发机制

消息转发源码

在函数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这个函数,然后再缓存,我们找下源码

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
	
	
	ENTRY _objc_msgSend_noarg
	b	_objc_msgSend
	END_ENTRY _objc_msgSend_noarg

	ENTRY _objc_msgSend_debug
	b	_objc_msgSend
	END_ENTRY _objc_msgSend_debug

	ENTRY _objc_msgSendSuper2_debug
	b	_objc_msgSendSuper2
	END_ENTRY _objc_msgSendSuper2_debug

	
	ENTRY _method_invoke
	// x1 is method triplet instead of SEL
	add	p16, p1, #METHOD_IMP
	ldr	p17, [x16]
	ldr	p1, [x1, #METHOD_NAME]
	TailCallMethodListImp x17, x16
	END_ENTRY _method_invoke

#endif

发现这是一段汇编源码,不太容易看懂,我们看看能不能通过系统日志看下,在分析源码中有个函数

log_and_fill_cache(cls, meth->imp, sel, inst, curClass);

写日志,进去看下

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}

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这个字段控制,那么我们看下这个字段的控制位置

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

找到了,这个函数,我们可以用这个函数,进行写日志操作

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LGStudent *student = [LGStudent alloc] ;

        // 动态方法决议
        // 对象方法
        // 类方法 -
        
        instrumentObjcMessageSends(true);
        [student saySomething];
        instrumentObjcMessageSends(false);

    }
    return 0;
}

运行程序后,打开日志文件/tmp/msgSends-11857

+ LGStudent NSObject resolveInstanceMethod:
+ LGStudent NSObject resolveInstanceMethod:
- LGStudent NSObject forwardingTargetForSelector:
- LGStudent NSObject forwardingTargetForSelector:
- LGStudent LGStudent methodSignatureForSelector:
- LGStudent LGStudent methodSignatureForSelector:
+ NSString NSObject alloc
+ NSString NSString allocWithZone:
- NSPlaceholderString NSPlaceholderString initWithBytesNoCopy:length:encoding:freeWhenDone:

发现在系统又调用了两个函数forwardingTargetForSelector,methodSignatureForSelector,那么这两个函数做什么用呢?

官方文档解释

forwardingTargetForSelector

第一次转发.png

可以看出

  • forwardingTargetForSelector进行第一次转发,即快速转发
  • 该方法的返回对象是执行sel的新对象,也就是自己处理不了会将消息转发给别人的对象进行相关方法的处理,但是不能返回self,否则会一直找不到
  • 该方法的效率较高,如果不实现或者nl,会走到forwardInvocation:方法进行处理
  • 底层会调用objc_msgSend(forwardingTarget, sel, ...);来实现消息的发送
  • 被转发消息的接受者参数和返回值等需要和原方法相同

代码验证: 我们先定义新的类,让这个类实现接口saySomething

@interface LGTeacher : NSObject

@end

@implementation LGTeacher

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

@end

然后在LGStudent类中实现消息转发代码

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) {
        return [LGTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

最后验证

  LGStudent *student = [LGStudent alloc] ;
  [student saySomething];

输出结果为:

2020-01-07 11:44:50.947183+0800 008-方法查找-消息转发[19647:237077] -[LGStudent forwardingTargetForSelector:] -- saySomething
2020-01-07 11:44:50.948811+0800 008-方法查找-消息转发[19647:237077] -[LGTeacher saySomething]

methodSignatureForSelector

屏幕快照 2020-01-07 下午1.09.32.png
从文档可以看出

  • 该方法是让我们根据方法选择器SEL生成一个NSMethodSignature方法签名并返回,这个方法签名里面其实就是封装了返回值类型,参数类型等信息。
  • forwardInvocation和methodSignatureForSelector必须是同时存在的,底层会通过方法签名,生成一个NSInvocation,将其作为参数传递调用
    屏幕快照 2020-01-07 下午1.33.23.png
    从文档可以看出
  • 查找可以响应 InInvocation中编码的消息的对象。对于所有消息,此对象不必相同
  • 使用 anInvocation将消息发送到该对象。anInvocation将保存结果,运行时系统将提取结果并将其传递给原始发送者。

代码验证

//慢速转发 -- 漂流瓶
// unre - 不崩溃
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

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

    // 事情 - 事务 - 秘书 - 失效
    // 系统本质
   SEL aSelector = [anInvocation selector];

   if ([[LGTeacher alloc] respondsToSelector:aSelector])
       [anInvocation invokeWithTarget:[LGTeacher alloc]];
   else
       [super forwardInvocation:anInvocation];
    }
}

运行结果为:

2020-01-07 13:38:08.175563+0800 008-方法查找-消息转发[31283:593368] -[LGStudent methodSignatureForSelector:] -- saySomething
2020-01-07 13:38:08.177614+0800 008-方法查找-消息转发[31283:593368] -[LGStudent forwardInvocation:]
2020-01-07 13:38:08.178789+0800 008-方法查找-消息转发[31283:593368] -[LGTeacher saySomething]

总结

来张图片总结下

屏幕快照 2020-01-07 下午1.42.43.png