美文网首页iOS基础iOS专题
iOS基础(八) - runtime之消息传递

iOS基础(八) - runtime之消息传递

作者: 一剑孤城 | 来源:发表于2017-03-13 14:09 被阅读42次

    前言:本来是想整理一下runtime相关的知识的,谁知道,越陷越深,一个知识点连着一个知识点,我怕以后忘记,所以,先记下来了,谁叫我比较懒呢。😄

    1.iOS里面的函数调用其实就是消息转发的过程。(How to prove?)

    先新建一个继承NSObject的类ClassA,里面有一个实例方法printStr:方法和类方法classMethod:,如下:

    @implementation ClassA
    
    + (void)classMethod: (NSString *)str {
        NSLog(@"%@", str);
    }
    
    - (void)printSomething: (NSString *)str {
        NSLog(@"%@", str);
    }
    
    @end
    
    //main.m 在main里调用
    [ClassA classMethod: @"classMethod"];
    ClassA *classA = [ClassA new];
    [classA printStr: @"instanceMethod"];
    

    将main.m编译成C++源文件

    clang -rewrite-objc main.m
    

    编译结果如下:

    //类方法调用
    ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)objc_getClass("ClassA"), sel_registerName("classMethod:"), (NSString *)&__NSConstantStringImpl__var_folders_pn_xqk2zmnx559bmvnwm35rzb4h0000gn_T_main_2b6623_mi_0);
    
    //实例方法调用
    ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)classA, sel_registerName("printStr:"), (NSString *)&__NSConstantStringImpl__var_folders_pn_xqk2zmnx559bmvnwm35rzb4h0000gn_T_main_2b6623_mi_1);
    

    可以看出来,调用类方法和实例方法最后都会转换成objc_msgSend方法的调用,所以,objc_msgSend是何方神圣?别急,先找到objc/message.h头文件,如下:

    /** 
     * Sends a message with a simple return value to an instance of a class.
     * 
     * @param self A pointer to the instance of the class that is to receive the message.
     * @param op The selector of the method that handles the message.
     * @param ... 
     *   A variable argument list containing the arguments to the method.
     * 
     * @return The return value of the method.
     * 
     * @note When it encounters a method call, the compiler generates a call to one of the
     *  functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
     *  Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper; 
     *  other messages are sent using \c objc_msgSend. Methods that have data structures as return values
     *  are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
     */
    OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
        OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
    

    可以看的出来,实例方法转换成了相应的消息传递。那么类方法呢?还记得我写的上一篇文章《聊一聊NSObject对象模型》里面的那张instance-class-metaClass之间的关系不?里面instance的isa指针指向的是class,而class的isa指针指向的是metaClass,我的理解就是可以把class看作metaClass的instance,那么类方法和实例方法的调用完全一致了。

    2.消息传递的过程

    根据苹果官方文档,消息转发的过程如下(这里说的是实例方法的调用,类方法类似):

    1.通过实例的isa指针找到相应的class。
    2.在cache(这里存放的都是函数实现,一旦函数被调用就会保存在缓存中)中以selector为key查找相应的函数imp,若找到则直接调用,否则进行下一步。
    3.在table(这里存放的是类的所有的函数列表)中继续寻找相应的函数实现,若找到则直接调用,否则进行下一步。
    4.通过class的superclass指针找到父类,回到第2步。
    5.直到找到根类,还是没有找到相应的函数实现,则开始进行消息转发。

    我们来看一下苹果官方文档的说明:

    When a message is sent to an object, the messaging function follows the object’s isa pointer to the class structure where it looks up the method selector in the dispatch table.
    If it can’t find the selector there, objc_msgSend follows the pointer to the superclass and tries to find the selector in its dispatch table. Successive failures cause objc_msgSend to climb the class hierarchy until it reaches the NSObject class.
    Once it locates the selector, the function calls the method entered in the table and passes it the receiving object’s data structure.

    To speed the messaging process, the runtime system caches the selectors and addresses of methods as they are used.
    There’s a separate cache for each class, and it can contain selectors for inherited methods as well as for methods defined in the class.
    Before searching the dispatch tables, the messaging routine first checks the cache of the receiving object’s class (on the theory that a method that was used once may likely be used again).
    If the method selector is in the cache, messaging is only slightly slower than a function call.
    Once a program has been running long enough to “warm up” its caches, almost all the messages it sends find a cached method. Caches grow dynamically to accommodate new messages as the program runs.

    第一段,主要讲了消息转发的整个过程,其中dispatch table就是一个函数分发列表,整个过程如下图:


    messaging.png

    第二段,主要讲的为了加速函数访问,类里面的函数一旦被调过之后会存进缓存里面,这样,访问函数先访问缓存,就可以提高访问速度。

    3.消息的转发机制

    在第二部的消息传递如果没有找到合适的对象处理当前的消息,则开始消息转发流程,如下:

    消息转发.png

    消息动态处理(resolveInstanceMethod)

    void dynamicMethod(id self, SEL aSelector, id argument) {
        NSLog(@"%@", argument);
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(test:)) {
            class_addMethod([self class], sel, (IMP)dynamicMethod, "v@:@");
            return YES;
        }
        return [super resolveInstanceMethod: sel];
    }
    //参数:v -> void @ -> id  : -> selector
    //也就是说dynamicMethod返回值为空,参数为对象的函数
    //更多的资料请查询苹果官方文档《Type Encodings》
    

    假如消息动态处理返回NO,则进入消息转发流程,响应目标转移(forwardingTargetForSelector)

    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if ([ProxyObject instanceMethodForSelector: aSelector]) {
            return [ProxyObject new];
        }
        return [super forwardingTargetForSelector: aSelector];
    }
    //当前对象不能响应转发的消息,系统会寻找代替的对象响应改消息
    

    假如消息响应目标返回nil,则系统会生成函数的签名,进行最后的处理(forwardInvocation)

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if ([ProxyObject instancesRespondToSelector: aSelector]) {
            return [ProxyObject instanceMethodSignatureForSelector: aSelector];
        }
        return [super methodSignatureForSelector: aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        if ([ProxyObject instancesRespondToSelector: anInvocation.selector]) {
            [anInvocation invokeWithTarget: [ProxyObject new]];
        } else {
            [super forwardInvocation: anInvocation];
        }
    }
    //方法签名为nil,则不会调forwardInvocation函数,直接调doesNotRecognizeSelector函数,报异常
    //方法签名不为nil,则会调用forwardInvocation函数,进行最后的处理
    

    直到最后消息都没有被处理,则调用doesNotRecognizeSelector函数,报异常

    - (void)doesNotRecognizeSelector:(SEL)aSelector {
      ...
    }
    

    Tips:
    1.消息转发的方法,默认会调父类响应的方法。
    2.函数前面如果返回nil,则不会调用forwardInvocation方法,直接报异常。
    3. resolveInstanceMethod方法,会在组装函数签名时重新被调用;如果函数签名返回nil,则不会被调用。

    总结

    这里都是一些关于runtime消息传递的比较浅显的解释,如果感兴趣想要更深入研究,推荐一篇文章《神经病院Objective-C Runtime住院第二天——消息发送与转发》里面讲的非常的详细,深入,不过,看懂的话有点难度,需要懂一点点汇编。

    参考:

    Objective-C Runtime Programming Guide
    iOS runtime 之消息转发
    Objective-C Runtime 运行时之三:方法与消息
    iOS开发之Runtime运行时机制

    相关文章

      网友评论

        本文标题:iOS基础(八) - runtime之消息传递

        本文链接:https://www.haomeiwen.com/subject/htbqgttx.html