iOS 探索系列相关文章 :
iOS 探索 -- alloc、init 与 new 的分析
iOS 探索 -- KVC 原理分析 前面介绍了消息查找失败时的
动态方法决议
, 如果动态方法决议
仍然没有解决问题, 在最后就会进入到消息转发流程
中, 接下来就来探索一下消息转发流程的相关实现:
1. 开始消息转发
那么是在哪个地方进入到消息转发流程的呢 ?
在消息查找的慢速流程方法 lookUpImpOrForward
方法中的结尾处有这样两行代码:
// No implementation found, and method resolver didn't help.
// 没有找到方法的实现 imp, 并且动态方法决议也没有帮助解决问题
// Use forwarding.
// 触发消息转发流程
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
从上面的注释就能够明白, 这里就是进入消息转发的入口, 那么这里主要做了什么呢? _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
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
// _objc_foward_handler 的实现
// 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;
搜索到 _objc_msgForward_impcache
是一个汇编实现, 最终来到了 objc_defaultForwardHandler
方法里面, 这个方法是一个报错方法, 所以看来这个探索流程是行不通的, 那么改用什么方法呢?
在网上搜索别人的经验时看到了这样一种方式, 在查找到方法缓存的时候系统会调用 log_and_fill_cache
方法, 在方法里面有一个记录日志的地方:
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);
}
// logMessageSend 方法
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
// 将日志输出到 "/tmp/msgSend-%d" 目录下面
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;
}
试着尝试用这个方法来查看一下方法的调用日志 (日志被保存在了 /private/tmp 目录下), 调用这个方法之前还要解决一个问题, 就是 objcMsgLogEnabled 默认为 false
, 想要 log_and_fill_cache
调用记录日志方法就必须改为 true
。全局搜索一下 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 {
Student *s = [[Student alloc] init];
instrumentObjcMessageSends(true);
// sayNB 方法没有实现
[s sayNB];
instrumentObjcMessageSends(false);
}
return 0;
}
程序崩溃后来到 /private/tmp
目录下看到了被缓存的文件:
打开后的文件内容:
可以看到在调用完方法决议 resolveInstanceMethod
后, 又调用了 forwardingTargetForSelector 和 methodSignatureForSelector
两个方法, 然后后面有调用了 doesNotRecognizeSelector
让程序崩溃。
所以可以大概推测一下这两个方法就是要研究的消息转发调用的方法, 下面来看看这两个方法
2. 消息转发研究
1. 消息转发流程图
为了方便后面对于消息转发的理解, 这里先提前把消息转发的完整流程图放出来吧:
2. 快速消息转发流程
先来看一下消息的快速转发流程, 也就是 forwardingTargetForSelector
方法, 官方文档的解释是这样的:
给出的解释是该方法返回的是未找到消息应该被首先定向到的对象 , 意思就是把当前消息交给了能够处理该消息的对象。下面在代码中尝试一下看看:
// Student 类
@interface LGStudent : NSObject
- (void)sayHello;
+ (void)sayObjc;
@end
@implementation Student
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello)) {
return [Person alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
// Person 类
@implementation Person
- (void)sayHello{
NSLog(@"%s",__func__);
}
@end
// main.m
Student *student = [[Student alloc] init];
[student sayHello];
// 结果
- [Person sayHello];
结果发现真的转发到了 Person
类的实现当中, 并且成功调用了 Person
类中的实现。另外在官方文档中还有下面这些说明需要注意一下:
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 returnself
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.
如果您在非根类中实现此方法,如果您的类对于给定的选择器没有要返回的内容,那么您应该返回调用super实现的结果。
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
方法的最后返回结果决定了将当前消息交给谁处理, 返回结果是 一个其他对象。如果返回结果是self
的话会进入无线循环, 如果返回 nil 的话就没有任何意义了。- 如果自己实现了该方法, 但是没有要返回的对象的话, 在最后应该返回
[super forwardingTargetForSelector:aSelector]
而不是返回 self。 - 此方法对于想要将消息转发出去的处理是可行的, 并且他的速度非常快。但是如果别的处理方式的话, 他就没有用了。
3. 慢速消息转发流程
慢速转发流程中包含两个方法 methodSignatureForSelector
和 forwardInvocation
, 并且他们两个应该是一起出现的。在官方文档中有一段话证明了这一点, 是这样说的:
To respond to methods that your object does not itself recognize, you must override
methodSignatureForSelector:
in addition toforwardInvocation:
. The mechanism for forwarding messages uses information obtained frommethodSignatureForSelector:
to create theNSInvocation
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.要响应对象本身无法识别的方法,除了forwardInvocation外,还必须重写methodSignatureForSelector:。转发消息的机制使用从methodSignatureForSelector获得的信息:创建要转发的NSInvocation对象。您的重写方法必须为给定的选择器提供适当的方法签名,可以通过预先制定一个方法签名,也可以通过向另一个对象请求一个方法签名。
下面来看看怎么使用:
// 前面的类实现部分省略掉了, 这里直接看转发实现
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL aSelector = [anInvocation selector];
if ([[Person alloc] respondsToSelector:aSelector]) {
[anInvocation invokeWithTarget:[Person alloc]];
}
// 如果 if 判断未通过就会来到这里
// 这里没有做任何操作的话就会调用方法失败, 但是不会崩溃
// 所以在这里也可以做一些容错处理
}
来看一下慢速流程:
- 消息转发到
methodSignatureForSelector
, 返回值是一个方法的签名 - 然后来到方法
forwardInvocation
, 参数是一个被调用对象。然后在代码里做了一次判断看Person
类中有没有这个 被调用对象 的 SEL 的实现 - 如果有实现的话, 就用 [Person alloc] 也就是 Person 的实例对象去调用这个 selector
- 如果没有就什么都不做, 而且这里是不会崩溃的哦, 就只是什么都没有了。
3. 总结
本次主要是研究了消息转发的几个流程, 可以大致概括成以下几条:
- 在
方法动态决议
完之后如果仍然没有完成消息查找, 就会进入消息转发流程 - 消息转发流程有两个, 分别是
快速转发流程
和慢速转发流程
- 快速转发流程通过实现
forwardingTargetForSelector
方法, 将消息转发给可以处理该消息的其它对象来完成消息查找 - 慢速流程要同时实现
methodSignatureForSelector
和forwardInvocation
两个方法, 前面一个方法返回一个方法签名, 在后面一个方法里判断能不能调用该方法; 如果能够调用就去调用, 不能调用就不做处理 - 最后还要说一点就是, 在研究的时候是将 快速流程 和 慢速流程 拆开来研究的, 如果同时实现这两种流程的话就要按照上面的
消息转发流程图
视情况而定了