iOS Objective-C 消息的转发
1.动态方法决议(解析)
在上一篇消息查找的文章中我们在消息查找中没有找到的消息就会进入动态方法决议代码中。为了连贯性,本篇中会重新且详细的讲解一下动态方法决议。
1.1 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 (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
resolveMethod_locked
主要作用是判断类是否是元类
- 如果不是则进入
resolveInstanceMethod
继续处理 - 如果是则进入
resolveClassMethod
继续处理,并且通过lookUpImpOrNil
判断非空,最后也会调用resolveInstanceMethod
进行对象方法的动态决议,因为根据isa
走位图,万物皆对象,最终都会继承自NSObject
,最后会找到NSObject
的对象方法中。
1.2 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());
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// 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 = 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));
}
}
}
该函数实质是做了一次方法的决议操作
- 初始化一个sel
resolveInstanceMethod
- 然后查找该sel,找到后则继续处理(找到说明实现了该方法),找不到就直接返回
- 通过
objc_msgSend
发送消息,这里发送的是resolveInstanceMethod
消息,如果返回YES
则说明该方法被实现,否则未实现。 - 如果实现并且决议处做了转发,说明该
sel
指向了新的imp
,并通过下面的打印来说明新IMP
被动态实现,或者没找到。
举个例子:
声明一个saySomething
的对象方法,但是没有实现,直接调用肯定会报方法找不到的错误,那么上述流程要怎样处理才能不报错呢?
实现代码如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"来了老弟:%s - %@",__func__,NSStringFromSelector(sel));
if (sel == @selector(saySomething)) {
NSLog(@"说话了");
IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
const char *sayHType = method_getTypeEncoding(sayHMethod);
return class_addMethod(self, sel, sayHIMP, sayHType);
}
return [super resolveInstanceMethod:sel];
}
当我们调用saySomething
时,因为没有实现所以找不到该方法,当我们实现了resolveInstanceMethod
后,并在其内部将saySomething
的imp
指定为我们已经实现了的sayHello
方法,就不会引起崩溃,最终就会调用sayHello
,这就是runtime
给开发者留下的对于对象方法的一种容错处理。
1.3 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 (!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;
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 = 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));
}
}
}
该函数跟resolveInstanceMethod
差不多,唯一的区别就是发消息的时候是向元类发送消息。其余的就不在赘述了。
举个例子:
跟对象方法的例子一样首先声明一个sayLove
的类方法,然后没有实现。调用后肯定还是会崩溃,这里我们在resolveClassMethod
方法中对齐进行处理。
实现代码如下:
+ (BOOL)resolveClassMethod:(SEL)sel{
if (sel == @selector(sayLove)) {
// 类方法在元类 objc_getMetaClass("LGStudent")
NSLog(@"说- love");
IMP sayOIMP = class_getMethodImplementation(objc_getMetaClass("LGStudent"), @selector(sayObjc));
Method sayOMethod = class_getClassMethod(objc_getMetaClass("LGStudent"), @selector(sayObjc));
const char *sayOType = method_getTypeEncoding(sayOMethod);
return class_addMethod(objc_getMetaClass("LGStudent"), sel, sayOIMP, sayOType);
}
return [super resolveClassMethod:sel];
}
实现原理跟对象方法的实现也基本差不多当我们调用sayLove
时,因为没有实现所以找不到该方法,当我们实现了resolveClassMethod
后,并在其内部将sayLove
的imp
指定为我们已经实现了的sayObjc
方法,就不会引起崩溃,最终就会调用sayObjc
,这就是runtime
给开发者留下的对于类方法的一种容错处理。这里有一点需要特别注意,就是类方法是存储在原类中的,无论使我们获取sayObjc
时还是添加新的方法时都应该选择元类进行处理,否则就会找不到方法,从而触发resolveInstanceMethod
对象方法的动态决议,如果还是找不到就会崩溃。如果在NSObject
中,或者NSObject
的分类中实现了resolveInstanceMethod
并且使用同样的放处理sayLove
,这时候同样可以解决由sayLove
没有实现而引起的崩溃。实现代码如下:(NSObject
分类中实现)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(sayLove)) {
NSLog(@"说话了");
IMP sayHIMP = class_getMethodImplementation(self, @selector(sayEasy));
Method sayHMethod = class_getInstanceMethod(self, @selector(sayEasy));
const char *sayHType = method_getTypeEncoding(sayHMethod);
return class_addMethod(self, sel, sayHIMP, sayHType);
}
return NO;
}
为什么可以这样:
主要原因是resolveMethod_locked
中这两句代码决定的。上个isa
走位图就会更加清晰。
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
isa流程图
由这个流程图我们可以知道,元类最终继承自根元类,根元类又继承自NSObject
,我们的方法(消息)在原类中也是以对象方法的形式存在的,当调用lookUpImpOrNil
时会递归查找父类的方法列表,我们无法操作元类以及根元类,因为它们是系统生成的,但是我们可以借助NSObject Category
的方式来实现方法的动态决议。如果类实现了方法的动态决议就不会到这里,如果没实现才会到NSObject
的方法动态决议。
2. 消息转发
2.1 _objc_msgForward_impcache
如果所有地方均没有实现方法的动态决议,那么我们的底层还会有什么处理呢?
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
在lookUpImpOrForward
方法的一开始我们就初始化了如上代码所示的imp
。当找不到方法且没有实现动态决议的相关处理,最后会将此sel
与_objc_msgForward_impcache
进行配对,进入消息的转发流程,如下图。
我们搜索_objc_msgForward_impcache
最终又来到objc-msg-arm64.s
文件处。代码如下:
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
2.2 _objc_msgForward
通过源码我们可以看出__objc_msgForward_impcache
内部实际是调用了_objc_msgForward
,紧跟其后的源码就是__objc_msgForward
,下面我们继续探索
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
2.3 通过打印日志寻找流程
看了__objc_msgForward
的源码并没有什么像objc_msgSend
那样的有用信息,这里我们并不能发现什么,一时间仿佛线索断裂,苹果爸爸只是开源到如此地步,那么我们该如何研究消息转发的详细流程呢?回想以前的的步骤找到imp
后会继续进行缓存的填充和日志的打印,在我们的开发过程中往往都会通过日志的打印来发现和解决问题,那么我们不妨看看日志都打印了什么。
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill(cls, sel, imp, receiver);
}
/// logMessageSend
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
为true
的时候日志会输出到/tmp/msgSends-xxx
的目录下。那么该如何让objcMsgLogEnabled
为true
呢,我们先不妨搜索一下,搜完后我们发现改变objcMsgLogEnabled
的值是通过一个名字叫instrumentObjcMessageSends
的函数。
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;
}
那么我们就来试一试,首先要新建一个MacOS工程,然后extern
一下,否则不能调用。调用完毕后我们来到/private/tmp
目录下
首先我们就看到了我们熟悉的resolveInstanceMethod
,紧接着就是forwardingTargetForSelector
和methodSignatureForSelector
这两个方法我们就没见过了。然后就是doesNotRecognizeSelector
,这个方法是打印日志的方法。我们来到objc4-779.1
的源码中搜索这个几个方法,实现代码如下:
+ (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
- (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("+[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}
// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("-[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}
// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
class_getName(self), sel_getName(sel), self);
}
// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
这时候我们发现了unrecognized selector sent to instance
这就是我们常见的崩溃错误的打印实现了。在这个打印完毕后我们在刚才查看日志的工程中的控制台还看到了如下的日志:
在控制台日志中我们看到了CoreFoundation
框架中的___forwarding___
的调用,但是我们知道CoreFoundation
并没有开源很多,那么我们先看看官方文档,先查看一下forwardingTargetForSelector
和methodSignatureForSelector
2.4 forwardingTargetForSelector(快速转发流程)
forwardingTargetForSelector根据文档的释义,此方法是返回一个能够定位到未找到消息imp
的对象(object),也就是说,这个对象没有实现该方法,那么就去找另一个对象。
举个例子:
我们在刚才打印日志的工程中在实现一个LGteacher
的类,再其内部实现saySomething
方法,然后在LGStudent
中添加如下代码:
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(saySomething)) {
return [LGTeacher alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
其实就是在forwardingTargetForSelector
中实现了狸猫换太子的操作,切实应用了苹果官方文档的解释,返回了一个实现了该方法对象。打印结果如下:
根据打印结果我们可以知道LGStudent
实例对象发送的saySomething
消息最后由LGteacher
响应。关于forwardingTargetForSelector
苹果的官方文档还给出了几点提示如下:
Discussion(讨论)
If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, that returned object is used as the new receiver object and the message dispatch resumes to that new object. (Obviously if you return self from this method, the code would just fall into an infinite loop.)
译:如果一个对象实现(或继承)这个方法,并返回一个非
nil
(和非self
)结果,那么返回的对象将用作新的接收者对象,消息分派将继续到这个新对象。(显然,如果从这个方法返回self
,代码将陷入无限循环。)
If you implement this method in a non-root class, if your class has nothing to return for the given selector then you should return the result of invoking super’s implementation.
译:如果你在一个非根类中实现这个方法,并且你的类对于给定的选择器没有返回任何东西,那么你应该返回父类的实现的结果。
This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding.
译:此方法让对象有机会在开销大得多的
forwardInvocation:
机械接管之前重定向发送给它的未知消息。 当您只是想将消息重定向到另一个对象时,这是非常有用的,并且可能比常规转发快一个数量级。如果转发的目标是捕获NSInvocation
,或者在转发过程中操纵参数或返回值,那么它就没有用了。
小结:
-
forwardingTargetForSelector
是一个更快的转发消息的流程,它能直接让其他可以响应的对象来响应未知消息。 -
forwardingTargetForSelector
不能反回self
不然就会陷入死循环。 - 在非根类中实现该方法对于给定的选择器没有实现任何东西,则需要返回父类的实现也结果。
-
forwardingTargetForSelector
适用于将消息转发给其他可以响应的该消息的对象,其主要的意思就是返回值和参数必须都一样,否则还要进行其他流程。
2.5 methodSignatureForSelector(慢速转发流程)
我们还是先看看methodSignatureForSelector
的官方文档
这里的释义是methodSignatureForSelector
返回一个NSMethodSignature
类型的方法签名对象,该对象包含由给定选择器标识的方法的描述。这里只是个方法签名,对参数和返回值没有要求,这就是在forwardingTargetForSelector
小结里面说的其他流程。
Discussion(讨论)
This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature.
译:该方法用于协议的实现。同时这个方法也用于必须创建
NSInvocation
对象的情况,比如在消息转发期间。如果您的对象维护一个委托或能够处理它没有直接实现的消息,您应该重写此方法以返回适当的方法签名。
在文档的末尾处我们还看到有一个叫forwardInvocation
的方法,我们点进去看看
根据我文档的定义:在子重写以将消息转发给其他对象。
Discussion(讨论)
When an object is sent a message for which it has no corresponding method, the runtime system gives the receiver an opportunity to delegate the message to another receiver. It delegates the message by creating an NSInvocation object representing the message and sending the receiver a forwardInvocation: message containing this NSInvocation object as the argument. The receiver’s forwardInvocation: method can then choose to forward the message to another object. (If that object can’t respond to the message either, it too will be given a chance to forward it.)
译:当向对象发送没有对应方法的消息时,运行时系统给接收方一个机会将消息委托给另一个接收方。它通过创建一个表示消息的
NSInvocation
对象并向接收者发送一个包含这个NSInvocation
对象作为参数的forwardInvocation:
消息来委托消息。然后,接收方的forwardInvocation:
方法可以选择将消息转发到另一个对象。(如果该对象也不能响应消息,那么它也将获得一个转发消息的机会。)
The forwardInvocation: message thus allows an object to establish relationships with other objects that will, for certain messages, act on its behalf. The forwarding object is, in a sense, able to “inherit” some of the characteristics of the object it forwards the message to.
译:因此,
forwardInvocation: message
允许对象与其他对象建立关系,对于某些消息,这些对象将代表它行事。在某种意义上,转发对象能够“继承”它所转发消息的对象的某些特征。
Important(划重点)
To respond to methods that your object does not itself recognize, you must override methodSignatureForSelector: in addition to forwardInvocation:. The mechanism for forwarding messages uses information obtained from methodSignatureForSelector: to create the NSInvocation object to be forwarded. Your overriding method must provide an appropriate method signature for the given selector, either by pre formulating one or by asking another object for one.
译:为了响应对象本身不能识别的方法,您必须重写
methodSignatureForSelector:
和forwardInvocation:
。转发消息的机制使用methodSignatureForSelector:
获得的信息来创建要转发的NSInvocation
对象。重写方法必须为给定的选择器提供适当的方法签名,可以通过预先构造一个选择器,也可以通过向另一个对象请求一个选择器。
显然methodSignatureForSelector
和 forwardInvocation
是要一起出现的,下面我们通过一个示例来演示如何使用这两个方法来实现消息的转发。
举个例子:
还是刚才的工程,注释掉forwardingTargetForSelector
的实现。
- (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];
}
}
打印结果如下:
打印结果可以看到,通过以上代码的处理saySomething
消息也被转发了。其实当我们注释了forwardInvocation
内部实现,也不会导致崩溃。
(文档上的其他注意点总结)其他注意点:
-
forwardInvocation
可以查找响应anInvocation
中的编码的消息对象,对于所有消息,此对象不必相同 - 使用
anInvocation
将消息发送到该对象时anInvocation
将保存结果,运行时系统将提取结果并将其传递给原始发送者 -
forwardInvocation
方法的实现不仅仅可以转发消息,还可以合并响应各种不同消息的代码,从而避免为每个选择器编写单独方法的麻烦。 -
forwardInvocation
方法对给定消息的响应中不仅将其转发给一个对象,还有可能涉及其他几个对象 -
forwardInvocation
是NSObject
的方法,并且只会调用doesNotRecognizeSelector
方法,如果不实现doesNotRecognizeSelector
它不会转发任何消息从而引起异常。
2.6 消息转发流程图
从动态方法决议到消息的快速转发,再到消息的慢速转发流程如下:
消息转发流程图
至此我们的消息转发流程基本完毕
3. 总结
- 动态方法决议有对象方法动态解析
resolveInstanceMethod
和类方法动态解析resolveClassMethod
两种,都需要开发者去实现 - 消息转发同样分为快速消息转发
forwardingTargetForSelector
和慢速消息转发methodSignatureForSelector
- 慢速消息转发同时还需要开发者实现
forwardInvocation
方法 - 快速消息转发是让其他能响应的对象来响应未查找到的消息,对参数和返回值要求绝对匹配
- 慢速消息转发提供了更加细粒度的控制,首先会返回一个方法签名给
runtime
,然后通过anInvocation
保存结果,Runtime
会提取结果并将其传递给原始发送者
至此我们的消息或者说方法,在Objective-C
的底层实现由objc_msgSend
开始,探索了消息发送的流程,然后由消息找不到时的处理进入到了动态方法决议,然后通过_objc_msgForward_impcache
进入到消息的转发流程就结束了,探索过程比较粗糙,也会有些瑕疵,如有问题欢迎指正。。
网友评论