美文网首页
消息转发

消息转发

作者: 4ab5c73f365f | 来源:发表于2018-05-09 16:34 被阅读16次

    1. 消息查找

    Objective-C 具有很强的动态性,它将静态语言在编译和链接时期做的工作,放置到运行时来处理. Runtime就是这门语言的基础.

    在Objective-C中, 某个对象进行方法调用, 多被称作向对象发送消息。我们如果需要了解这个问题就需要向底层探究.我们平时编写的Objective-C代码,底层实现其实都是C\C++代码
    如图:


    OC-C++-.png

    所以我们通过向下探究来研究一下,示例如下:

    @interface Person : NSObject
    - (void)run;
    @end
    
    @implementation Person
    - (void)run {
        NSLog(@"person is running");
    }
    @end
    
    // ViewController.m
    - (void)viewDidLoad {
        [super viewDidLoad];
        Person *p = [[Person alloc] init];
        [p run];
    }
    
    

    那么如何将Objective-C代码转换为C\C++代码呢? 苹果为我们提供了工具:

    规范: 
    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
    
    操作: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.mm
    
    

    [person run] 经过编译后如下调用的是objc_msgSend方法

    static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
        ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
        Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("person_run"));
    }
    
    

    通过转变为C++代码我们发现

    ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("person_run"));
    

    objc_msgSend()函数被调用, 通过查看Runtime 找到对应的函数定义. OBJC_EXPORT id objc_msgSend(id self, SEL op, ...);self:消息的接收者 , op: 消息的方法名,C 字符串 ... :参数列表

    那么对象调用方法, 是怎么进行转发的呢, 我们特地准备了一张图来描述:

    消息转发机制.png

    我们可以看到消息的处理分为两个阶段, "动态方法决议" 和 "消息转发", 只有在动态方法决议期间找不到方法时候才会进行下一步. 那我们先探究一下第一个阶段是怎么进行的. 在研究第一个阶段之前, 我们再来看一张经典图:


    类继承.jpg oc类图.png oc继承图.png

    在这两张图中,京D巴拉巴拉(京DBLABLA)就是实例, 小汽车类就是类对象, 小汽车类指向小汽车元类.小汽车元类指向 根元类, 根元类指向自己形成闭环, 通过图中的关系, 我们知道了实例对象通过isa指针,找到类对象, 类对象通过isa指针找到元类, 元类通过isa指针找到根元类对象. 根元类isa指向自己形成闭环, 但是superclass 指向NSObject, 所以可以说NSObject 就是消息机制的核心.

    OK, 在我们了解了OC基本概念之后回归到函数本身

    ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("person_run"));
    
    

    通过cmd+shift+o快捷键输入函数名 找到objc/runtime.h objc_msgSend函数函数定义如下, 但是函数中参数的sel_registerName("person_run")是什么, 我们继续查找, 同样在objc/runtime.h 中找到定义. 该函数在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器.

    
    OBJC_EXPORT id objc_msgSend(id self, SEL op, ...);
    
    
    OBJC_EXPORT SEL _Nonnull sel_registerName(const char * _Nonnull str)
    
    

    我们既然了解了函数, 我们从新回顾一下上边的"OC RUNTIME的基本概念"这张图. 回顾后,我们继续探究Class是什么? id 关键字是什么, 老方法,通过cmd+shift+o快捷键输入函数名,我们发现如下

    typedef struct objc_class *Class;
    
    /// Represents an instance of a class.
    struct objc_object {
        Class _Nonnull isa OBJC_ISA_AVAILABILITY;
    };
    
    /// A pointer to an instance of a class.
    typedef struct objc_object *id;
    
    

    Class 和 id 都是 objc_object 结构体, 继续查看objc_object是什么 我们看到

    struct objc_class {
    Class _Nonnull isa OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        Class _Nullable super_class OBJC2_UNAVAILABLE;
        const char * _Nonnull name OBJC2_UNAVAILABLE;
        long version OBJC2_UNAVAILABLE;
        long info OBJC2_UNAVAILABLE;
        long instance_size OBJC2_UNAVAILABLE;
        struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
        struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
        struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
        struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
    #endif
    
    } OBJC2_UNAVAILABLE;
    
    // 最新版本objc4-208 显示如下
    struct objc_class {
        struct objc_class *isa;
        struct objc_class *super_class;
        const char *name;
        long version;
        long info;
        long instance_size;
        struct objc_ivar_list *ivars;
    
    #if defined(Release3CompatibilityBuild)
        struct objc_method_list *methods;
    #else
        struct objc_method_list **methodLists;
    #endif
    
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
    };
    
    

    在上边结构体中我们需要重点关注几个关键字

    1. isa 指向父类的指针.

    2. methodLists 一个类的方法分发表.

    当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系。


    iOS-类熟悉继承.png

    如上图所示的继承关系, 就意味着我们寻找一个实例方法, 在student找不到, 通过isa指针指向的Person, 在Person中继续寻找,直到NSObject. 例如 alloc 函数, 会通过isa 一层一层直到NSObject为止.

    objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果 没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依 此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。

    通过阅读神经病院 Objective-C Runtime 住院第二天:消息发送与转发 文章找到一段

    #include <objc/objc-runtime.h>
    
    id c_objc_msgSend( struct objc_class /* ahem */ *self, SEL _cmd, ...)
    {
        struct objc_class *cls;
        struct objc_cache *cache;
        unsigned int hash;
        struct objc_method *method;
        unsigned int index;
    
    if( self)
    {
        cls = self->isa;
        cache = cls->cache;
        hash = cache->mask;
        index = (unsigned int) _cmd & hash;
    do
    {
        method = cache->buckets[ index];
        if( ! method)
        goto recache;
        index = (index + 1) & cache->mask;
    }
    while( method->method_name != _cmd);
        return( (*method->method_imp)( (id) self, _cmd));
    }
        return( (id) self);
    
    recache:
        /* ... */
        return( 0);
    }
    
    

    通过C版本的objc_msgSend的源码版本,画了一张大致示意图:

    此图描述仅供参考, 实际查找方式没进行深入研究, 请勿严重拍砖

    iOS-实现消息转发.png iOS-isa-方法类方法.png iOS-消息转发-superclass.png

    通过遍历类对象方法, meta-class, 找不到匹配的方法, 通过superclass 继续在父类中继续同样的操作,找到后加入实例类对象的cache, 如果遍历到NSObject还是找不到就开始了 消息转发.

    2.消息转发

    进入消息转发就不得再展示一次该图.


    消息转发机制.png

    对象在收到无法响应的消息后,会调用其所属类的下列方法

    /**
    * 如果尚未实现的方法是实例方法,则调用此函数
    *
    * @param selector 未处理的方法
    *
    * @return 返回布尔值,表示是否能新增实例方法用以处理selector
    */
    + (BOOL)resolveInstanceMethod:(SEL)selector;
    /**
    * 如果尚未实现的方法是类方法,则调用此函数
    *
    * @param selector 未处理的方法
    *
    * @return 返回布尔值,表示是否能新增类方法用以处理selector
    */
    + (BOOL)resolveClassMethod:(SEL)selector;
    

    备援接收者 或者 快转发
    如果无法动态解析方法,运行期系统就会询问是否能将消息转给其他接收者来处理,对应的方法为

    /**
    * 此方法询问是否能将消息转给其他接收者来处理
    *
    * @param aSelector 未处理的方法
    *
    * @return 如果当前接收者能找到备援对象,就将其返回;否则返回nil;
    */
    - (id)forwardingTargetForSelector:(SEL)aSelector;
    
    

    完整的消息转发机制
    如果前面两步都无法处理消息,就会启动完整的消息转发机制。首先创建 NSInvocation 对象,把尚未处理的那条消息有关的全部细节装在里面,在触发 NSInvocation 对象时,消息派发系统(message-dispatch system)将会把消息指派给目标对象。对应的方法为

    /**
    * 获取指定selector的方法签名
    *
    * @param aSelector aSelector 未处理的方法
    *
    */
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    
    /**
    * 消息派发系统通过此方法,将消息派发给目标对象
    *
    * @param anInvocation 之前创建的NSInvocation实例对象,用于装载有关消息的所有内容
    */
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    
    

    这个方法可以实现的很简单,通过改变调用的目标对象,使得消息在新目标对象上得以调用即可。然而这样实现的效果与 备援接收者 差不多,所以很少人会这么做。更加有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另一个参数、修改 selector 等等。

    • Person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    - (void)run;
    + (void)eat;
    @end
    
    
    
    • Person.m
    #import "Person.h"
    #import <objc/runtime.h>
    #import "Teacher.h"
    
    @implementation Person
    
    // 并没有真正实现run 方法, 和 eat 方法
    
    void eatClass(id self, SEL cmd) {
        NSLog(@"类方法eat的实现");
    }
    
    void runClass(id self, SEL cmd) {
        NSLog(@"方法run的实现");
    }
    
    // first
    + (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(eat))
    {
        NSLog(@"eat static method 调用resolveClassMethod");
        BOOL success = class_addMethod(object_getClass([self class]), sel, (IMP)eatClass, "v@:");
        return success;
    }
        return [super resolveClassMethod:sel];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(run)) {
        NSLog(@"run method , 调用resolveInstanceMethod");
        BOOL success = class_addMethod([self class], sel, (IMP)runClass, "v@:");
        return success;
        // return NO; // 返回NO 会进行快转发.
    }
        return [super resolveInstanceMethod:sel];
    }
    
    // second
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        NSLog(@"forwardingTargetForSelector");
     if (aSelector == @selector(run)) {
        // return nil; // 返回nil 会进行完整转发.
        return [[Teacher alloc] init];
    } else {
        return [super forwardingTargetForSelector:aSelector];
      }
    }
    
    
    // third
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
        if (!signature) {
            if (sel_isEqual(aSelector, @selector(run))) {
            signature = [[[Teacher alloc] init] methodSignatureForSelector:aSelector];
            }
        }
        return signature;
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSString *sel = NSStringFromSelector(anInvocation.selector);
        if ([sel isEqualToString:@"run"]) {
            [anInvocation invokeWithTarget:[[Teacher alloc] init]];
        } else {
            [super forwardInvocation:anInvocation];
       }
    }
    
    

    demo 地址

    Luckycity

    实际应用

    防止 Unrecogized-selector

    参考

    Objective-C Runtime 运行时之三:方法与消息
    神经病院 Objective-C Runtime 住院第二天:消息发送与转发
    Effective Objective-C Notes:理解消息传递机制
    美团-技术专家-臧成威
    李明杰-讲师

    相关文章

      网友评论

          本文标题:消息转发

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