iOS底层-消息转发流程分析

4,574 阅读11分钟

欢迎阅读iOS底层系列(建议按顺序)

iOS底层 - alloc和init探索

iOS底层 - 包罗万象的isa

iOS底层 - 类的本质分析

iOS底层 - cache_t流程分析

iOS底层 - 方法查找流程分析

iOS底层 - 消息转发流程分析

iOS底层 - dyld是如何加载app的

iOS底层 - 类的加载分析

iOS底层 - 分类的加载分析

本文概述

本文旨在通过分析消息转发的动态决议,快速转发,慢速转发三个阶段,完善objc_msgsend的最后流程。

1.经典崩溃

unrecognized selector sent to instance 0x100524a90

先来看个很经典的崩溃打印。一般这个日志前部分还会给出所调用的方法,我们可以借此很快找到原因所在,可以说是相当贴心了。然而,

苹果在方便我们的同时,你是否想过这个日志具体是在什么时候打印的系统是靠什么来捕获这类型即将崩溃的信息开发者是否也可以捕获呢。这些都要从消息转发流程说起,看完分析或许你心里就有答案了。

2.消息转发初探

2.1 寻找切入点

消息发送后,经过一系列查找都没结果时,会进入动态方法决议_class_resolveMethod方法,上篇iOS底层 - 方法查找流程分析后面部分详细说明了这个流程。

因此消息转发的切入点比其他流程的清晰多了,它紧紧的跟在方法查找流程之后,我们可以直接来到_class_resolveMethod

2.2 动态方法决议

2.2.1 _class_resolveMethod

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

根据源码实现可以得出几个信息:

  • 方法不存在元类时,调用_class_resolveInstanceMethod
  • 方法存在元类时,调用_class_resolveClassMethod
  • 调用完_class_resolveClassMethod后,又查找方法流程,如果!nil,居然又调用一次_class_resolveInstanceMethod

这里先保留这个问题,为什么调用类方法动态决议后,还需要调用一次对象方法动态决议

2.2.2 _class_resolveInstanceMethod

1.先检查resolveInstanceMethod的imp是否为nil,如果为nil,直接返回。
但是不会为nil,因为NSObject默认实现了。因此容错是次要的,这里更主要的目的,是为了寻找子类是否实现。
2.重定义objc_msgSend,声明有3个参数,第三个参数为resolveInstanceMethod的参数,然后发送resolveInstanceMethod消息(经过第一步,缓存里面已经存在resolveInstanceMethod)。
3.经过resolveInstanceMethod处理,可能已经动态添加了imp。所以再次获取sel的imp是否存在。
如果存在则结束转发,不存在继续转发流程。

2.2.3 _class_resolveClassMethod

因为和_class_resolveInstanceMethod实现流程一致,不给出代码。只是需要在元类查找,并且外界调用resolveClassMethod方法

2.2.4 分析验证

实践才是检验真理的唯一标准。以上都是基于源码的分析,还是需要给出实际验证。

思路:在类中创建两个方法,一个实现一个不实现,外界调用未实现的方法,内部对此方法进行动态方法决议处理。

a.动态添加imp:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(work)) {
        NSLog(@"开始动态方法决议");
        Method method = class_getInstanceMethod(self, @selector(play));
        const char * types = method_getTypeEncoding(method);
        IMP imp = class_getMethodImplementation(self, @selector(play));
        return class_addMethod(self, sel, imp, types);
    }
    return [super resolveInstanceMethod:sel];
}

b.未动态添加imp:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(work)) {
        NSLog(@"开始动态方法决议");
//        Method method = class_getInstanceMethod(self, @selector(play));
//        const char * types = method_getTypeEncoding(method);
//        IMP imp = class_getMethodImplementation(self, @selector(play));        
//        return class_addMethod(self, sel, imp, types);
    }
    return [super resolveInstanceMethod:sel];
}

分析ab两种情况,可以得出结论:

  • 原未实现的方法动态添加imp后,会调用imp所对应的方法,转发成功。
  • 原未实现的方法没有动态添加imp,依然会崩溃,转发失败。
  • 转发失败情况下,居然resolveInstanceMethod会执行两次。

这里先保留两个问题,

  • 为什么动态方法决议失败后,崩溃前动态方法决议还要执行一次
  • 开发者是否可以使用动态方法决议来处理崩溃问题

果然,实践结果进一步验证了源码分析的结论,可是又抛出了新的问题。

当转发成功时,两者基本一致;当转发失败时,程序依然会崩溃,可崩溃前却又执行了一次resolveInstanceMethod,是否意味着崩溃前系统还有其他的处理呢。

2.3 快速转发流程

我们猜测动态方法决议失败后,系统还有其他的处理,可是源码到这里就戛然而止了,只是打印了一些异常情况。

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

到这里因为无路可走,大部分人确实可能放弃。

这边提供一个黑科技

extern void instrumentObjcMessageSends(BOOL);

这是系统提供的可以打印消息发送过程的函数,只需要在调用的方法前后打开和关闭此通道,就可以输出方法执行过程的函数名,并且以日志形式保存在系统的tmp目录下

其实,这个黑科技的来源在于消息查找流程的源码中。 在消息查找流程中,如果在方法列表中找到了方法,都会调用

log_and_fill_cache(cls, cls, meth, sel)

点进来看下,

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

cache_fill填充缓存之前,如果objcMsgLogEnabled日志记录器允许则记录调用过程。

按照此方法,点开tmp下的文件。

可以很明显看到几个方法:

  • forwardingTargetForSelector
  • methodSignatureForSelector
  • doesNotRecognizeSelector

先来看下forwardingTargetForSelector,直接搜索会发现没有提供源码

从崩溃堆栈也可以看出,forwardingTargetForSelector是在CoreFoundation框架内,属于未开源的代码。

一般研究未开源的代码,都是从苹果苹果的官方文档入手。

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:
machinery接管。当您只是想将消息重定向到另一个对象时,这是非常有用的,并且可能比常规转发快
一个数量级。如果转发的目标是捕获NSInvocation,或者在转发过程中操纵参数或返回值,那么它就
没有用了。

大概意思:forwardingTargetForSelector可以直接返回一个对象来接受消息,并且比常规转发快一个数量级。

注意:返回的对象将用作新的接收者对象,消息分派将继续到这个新对象。(显然,如果从这个方法返回self,代码将陷入无限循环。)

那么直接实现如下代码,并且动态方法决议不处理

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(work)) {
        return [CJStudent alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

调用返回对象的work方法,快速转发成功。

这里在保留一个问题,如果forwardingTargetForSelector返回的对象也不能处理消息,后续将如何处理?

2.4 慢速转发流程

从文档已知,如果forwardingTargetForSelector未处理,则交给开销大得多的forwardInvocation接管。那继续看下forwardInvocation的文档说明。

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来实现慢速转发,并且必须为给定的选择器提供适当的方法签名。

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对象的情况,比如
在消息转发期间。如果您的对象维护一个委托或能够处理它没有直接实现的消息,您应该
重写此方法以返回适当的方法签名。

大概意思:消息转发期间,需要重写methodSignatureForSelector来返回适当的方法签名。

doesNotRecognizeSelector: method; it doesn’t forward any messages. Thus, if you choose not to implement forwardInvocation:, sending unrecognized messages to objects will raise exceptions.
译:NSObject简单调用doesNotRecognizeSelector:方法;它不转发任何消息。因此,如果选择不实现forwardInvocation:,则向对象发送无法识别的消息将引发异常。

大概意思:慢速转发也失败时,系统会调用doesNotRecognizeSelector抛出异常。

那么直接实现如下代码,并且动态方法决议和快速转发都不处理:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
   if (aSelector == @selector(work)) { 
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
   SEL aSelector = [anInvocation selector];
   if ([[CJStudent alloc] respondsToSelector:aSelector])
       [anInvocation invokeWithTarget:[CJStudent alloc]];
   else
       [super forwardInvocation:anInvocation];
}

同样响应了work方法,慢速转发成功,但是动态方法决议处理两次。

这里在保留一个问题,为什么执行慢速消息转发时,动态方法决议还要执行一次,而快速转发却不需要

2.4 消息无法处理

从文档已知,如果forwardInvocation也未处理,则系统调用doesNotRecognizeSelector。直接来看此方法

- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

一系列消息转发都失败后,系统调用此方法,直接抛出那个经典崩溃。

3.问题汇总

1.为什么调用类方法动态决议后,还需要调用一次对象方法动态决议?

失败后,就会调用一次对象方法动态决议。这和为什么子类可以调用类方法来实现
NSObject的对象方法原因一致。

回到NSObject的原因也可以这样理解,系统给开发者一个窗口处理问题类方法的转发问
题,否则调用类方法确没有窗口处理,因为元类开发者操作不了。

2.开发者是否可以使用动态方法决议来处理崩溃问题

是可以的。只需要对捕获到方法名做个过滤,区分系统方法和自定义方式。但是这种
方式侵入型较强,且如果被子类重写,此功能将失效。

3.如果forwardingTargetForSelector返回的对象也不能处理消息,后续将如何处理?

如果返回的对象不能处理此消息,将进入这个对象对应的类的动态方法决议,然后继续走
这个类的消息转发流程,直到找到或者都找不到方法。

4.为什么执行慢速消息转发时,动态方法决议还要执行一次,而快速转发却不需要

结合后续汇编猜想,快速转发直接交给一个对象处理,慢速转发就像是漂流瓶一样把方式
扔出去,但扔出去之前做了一步方法签名。方法签名后,系统内部后续的调用栈,需要判
断`class_respondsToSelector`对签名加以验证,此方法内部又调用了
lookUpImpOrNil,因此又来到动态方法决议。

3.消息转发总结

这张图很形象的说明了消息转发流程。

总的来说:消息转发进入时,动态方法决议,快速转发,慢速转发三者按顺序处理这个消息,谁能成功处理则结束,不能成功处理继续下个流程,都不能处理抛出崩溃。

4.写在最后

以上就是消息转发的全过程,和objc_msgSend主流程,cache_t流程共同搭建起iOS消息发送的架构。

到此,对iOS的对象,isa,类,cache_t,objc_msgSend等分析告一段落。后续将要开启新的篇章,探索下程序启动的过程,dyld是如何加载的程序的。敬请关注。