美文网首页
iOS开发 Rumtime运行时之消息发送机制(一)

iOS开发 Rumtime运行时之消息发送机制(一)

作者: 路漫漫其修远兮Wzt | 来源:发表于2018-06-20 18:22 被阅读25次

    转载自:IOS开发工程师--周玉的博客:iOS开发 深入浅出Rumtime运行时之消息发送机制详解

    iOS开发 Rumtime运行时之消息发送机制(一)
    iOS开发 Runtime运行时之官方翻译--动态方法解析(二)
    iOS开发 Rumtime运行时之消息转发机制(三)

    iOS开发

    在Objective-C中,使用对象进行方法调用是一个消息发送的过程(Objective-C采用“动态绑定机制”,所以所要调用的方法直到运行期才能确定)。

    方法在调用时,系统会查看这个对象能否接收这个消息(查看这个类有没有这个方法,或者有没有实现这个方法),如果不能并且只在不能的情况下,就会调用下面这几个方法,给你“补救”的机会,你可以先理解为几套防止程序crash的备选方案,我们就是利用这几个方案进行消息转发,注意一点,前一套方案实现后一套方法就不会执行。如果这几套方案你都没有做处理,那么程序就会报错crash。

    正常运行的方法

    案例分析:首先创建person类

    Person.h
    
    这里写图片描述
    Person.m
    
    这里写图片描述

    运行:


    这里写图片描述

    如果调用的方法没有实现

    注释掉run方法的实现

    这里写图片描述

    运行报错:原因如下

    这里写图片描述

    于是我们要思考:为什么会报错?以及该怎么处理这类错误?
    由此我们要深入理解一些基本的概念才能知晓其中的原理

    Object,Class,MetaClass概念

    关系图: 详解看这里 – 深入浅出Runtime运行时之类与对象的结构

    这里写图片描述

    Class,Method,SEL,IMP概念

    //类
    typedef struct objc_class *Class; 
    //对象
    typedef struct objc_object {
        Class isa; 
    } *id;
    //方法名
    typedef struct objc_selector *SEL; 
    //IMP
    typedef id (*IMP)(id, SEL, ...);
    
    • Class含义
    struct objc_class {
        Class isa;  //指向父类或者元类
        Class super_class  ;    //父类
        const char *name  ;     //类名
        long version   ;    //版本
        long info  ;    //信息
        long instance_size  ;    //实例变量的大小
        struct objc_ivar_list *ivars  ;    //成员变量列表
        struct objc_method_list **methodLists  ;    //方法列表,存储的是Method类型的方法
        struct objc_cache *cache   ;    //调用过得方法的缓存
        struct objc_protocol_list *protocols   ;    //要遵守的协议列表
    } ;
    

    由此可见,Class 是指向类结构体的指针,该类结构体含有一个指向其父类类结构的指针,该类方法的链 表,该类方法的缓存以及其他必要信息。

    NSObject 的 class 方法就返回这样一个指向其类结构的指针。每一个类实例对象的第一个实例变量是一个指向该对象的类结构的指针,叫做 isa。通过该指针,对象可以访问它对应的类以及相应的父类。

    • Method含义
      注意这里所说的方法链表里面存储的是 Method 类型的。图一中 selector 就是指 Method 的SEL, address 就是指 Method 的 IMP。
    //Method方法结构体
    typedef struct objc_method *Method;
    
    struct objc_method {
        SEL method_name ;    //方法名,也就是selector.
        char *method_types ;    //方法的参数类型.
        IMP method_imp ;    //函数指针,指向方法具体实现的指针..也即是selector的address.
    } ;
    
    // SEL 和 IMP 配对是在运行时决定的.并且是一对一的.也就是通过selector去查询IMP,找到执行方法的地址,才能确定具体执行的代码.
    // 消息选标SEL:selector / 实现地址IMP:address 在方法链表(字典)中是以key / value 形式存在的
    

    一个方法 Method,其包含一个方法选标 SEL – 表示该方法的名称,一个 types – 表示该方法参数的类型, 一个 IMP - 指向该方法的具体实现的函数指针。

    • SEL 的含义:
      它是一个指向 objc_selector 指针,表示方法的名字/签名
    typedef struct objc_selector    *SEL;   //方法的名称--@selector(方法名)
    

    不同的类可以拥有相同的 selector,这个没有问题,因为不同类的实例对象 performSelector 相同的 selector 时,会在各自的消息选标(selector)/实现地址(address) 方法链表中根据 selector 去查找具体的 方法实现 IMP, 然后用这个方法实现去执行具体的实现代码。这是一个动态绑定的过程,在编译的时候, 我们不知道最终会执行哪一些代码,只有在执行的时候,通过 selector 去查询,我们才能确定具体的执行 代码。

    • IMP含义
    typedef id (*IMP)(id, SEL, ...);    //函数指针IMP,指向方法的实现的指针  -----和block结构一样 void (^block)(int,int);
    
    // IMP 函数指针,被指向的函数/方法,包含一个接收消息的对象id(self,指针),调用方法的选标SEL(方法名),以及...方法的个数,并返回一个id.
    // IMP是消息最终调用的代码,是方法真正实现的代码
    

    根据前面 id 的定义,我们知道 id 是一个指向 objc_object 结构体的指针,该结构体只有一个成员 isa,所 以任何继承自 NSObject 的类对象都可以用 id 来指代,因为 NSObject 的第一个成员实例就是 isa。

    至此,我们就很清楚地知道 IMP 的含义:IMP 是一个函数指针,这个被指向的函数包含一个接收消息的 对象 id(self 指针), 调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个 id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在C语言里面一样使用这个函数 指针。

    消息调用过程

    至此我们对方法调用有个大致的了解过程

    [Person run];
    

    对run方法的调用,编译器通过插入一些代码,将之转换为对方法具体实现IMP的调用,在Person类结构体中的方法链表中查找名称为run的SEL对象的具体方法实现找到.

    还有一个问题,编译器插入了什么代码呢?如果方法在方法链表中没有找到对应的IMP,又会如何?

    消息函数objc_msgSend

    编译器会将消息转换为对消息函数objc_msgSend的调用

    id objc_msgSend(id self, SEL op, ...);
    

    主要有两个参数:id 消息接收者 SEL 消息对象的方法名, …接收的消息的参数.

    /* 消息函数 */ //编译器会将消息转换为对消息函数objc_msgSend的调用
     id objc_msgSend(id self, SEL op, ...);
    // id objc_msgSend(id theReceiver, SEL theSelector, ...);
    // 三个参数:消息接收者 id  方法名 SEL  参数 ...
    // [person run]; -- objc_msgSend(person, @selector(run));
    

    SEL和IMP的动态绑定

    objc_msgSend消息函数做了动态绑定所需要的一切工作:

    1,它首先找到 SEL 对应的方法实现 IMP。因为不同的类对同一方法可能会有不同的实现,所以找到的 方法实现依赖于消息接收者的类型。

    2, 然后将消息接收者对象(指向消息接收者对象的指针)以及方法中指定的参数传递给方法实现 IMP。

    3,最后,将方法实现的返回值作为该函数的返回值返回。

    编译器会自动插入调用该消息函数 objc_msgSend 的代码,我们无须在代码中显示调用该消息函数。

    当 objc_msgSend 找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法 实现,同时,它还将传递两个隐藏的参数:消息的接收者以及方法名称 SEL。

    这些参数帮助方法实现获得 了消息表达式的信息。它们被认为是”隐藏”的是因为它们并没有在定义方法的源代码中声明,而是在代码 编译时是插入方法的实现中的。

    - (void)objc_msgSend(参数等) {
        //消息接收者
        id target = getTheReceiver(); 
        //方法名
        SEL method = getTheMethod();
        //参数
        ............
    
        //self指收到run消息的对象
        if (target == self || method == objc_msgSend) { 
            return nil;
        }
        //参数
        ..........
    
        //消息接收者去调用对应的方法,并返回对应的结果
        return [target performSelector:method]; 
    }
    

    通过SEL查找IMP的过程

    前面说了,objc_msgSend 会根据方法选标 SEL 在类结构的方法列表中查找方法实现 IMP。这里头有一 些文章,我们在前面的类结构中也看到有一个叫 objc_cache *cache 的成员,这个缓存为 高效率而存在 的。每个类都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。。

    //下面来剖析一段苹果官方运行时源码:
    static Method look_up_method(Class cls, SEL sel, BOOL withCache, BOOL withResolver) {
    
        // 1\. 声明IMP
        Method meth = NULL;
    
        // 2\. 从cache中查找
        if (withCache) {
            meth = _cache_getMethod(cls, sel,&_objc_msgForward_internal);
            if (meth == (Method)1) {
                // cache中包含了这个方法的话,就停止搜索
                // Cache contains forward:: . Stop searching.
                return NULL;
            }
    }
    
        // Ivar class_getInstaceMethod(Class cls, SEL name);
        // Ivar class_getClassMethod(Class cls, SEL name);
        // 3\. 如果找不到从方法列表中查找(包括类方法列表和对象方法列表)
        if (!meth) meth = _class_getMethod(cls, sel);
    
        // 4\. 将找到的方法缓存到cache中
        if (!meth  &&  withResolver) meth = _class_resolveMethod(cls, sel);
        return meth;
    }
    

    通过分析上面的代码,可以看到,查找时:

    1,首先去该类的方法 cache 中查找,如果找到了就返回它;

    2,如果没有找到,就去该类的方法列表中查找。如果在该类的方法列表中找到了,则将 IMP 返回,并将 它加入 cache中缓存起来。根据最近使用原则,这个方法再次调用的可能性很大,缓存起来可以节省下次 调用再次查找的开销。

    3,如果在该类的方法列表中没找到对应的 IMP,在通过该类结构中的 super_class指针在其父类结构的方法列表中去查找,直到在某个父类的方法列表中找到对应的 IMP,返回它,并加入 cache 中;

    4,如果在自身以及所有父类的方法列表中都没有找到对应的 IMP,则看是不是可以进行动态方法决议(后 面有专文讲述这个话题);

    5,如果动态方法决议没能解决问题,进入下面要讲的消息转发流程。

    总结

    到此为止,就晓得为什么会报那样的错误了吧

    reason: '-[Persion run]: unrecognized selector sent to instance 0x60000023b120'
    

    [Person run];运行时时,objc_msgSend函数通过消息发送机制,在对应的person对象中找不到run方法的实现,所以就报错了(后面再谈论动态方法处理和消息转发机制)

    相关文章

      网友评论

          本文标题:iOS开发 Rumtime运行时之消息发送机制(一)

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