zoukankan      html  css  js  c++  java
  • 二、Objective-C之Runtime的使用

    接上篇、说到了Runtime的基本认识、Runtime与Objective-C的关系、以及陈述了objc_msgSend的过程。并且留下了一个线索,这篇就是对上篇留下的线索objc_class结构体做分析学习。

    objc_class

    先看下这个结构体里面的定义:

    struct objc_class {
        Class isa;	//meta元类
    
    #if !__OBJC2__
        Class super_class;	//父类
        const char *name;	//类名
        long version;	//类的版本信息,默认为0
        long info;	//类信息,运行期使用的一些位标识
        long instance_size;	//实例变量大小
        struct objc_ivar_list *ivars;	//成员变量链表
        struct objc_method_list **methodLists;	//方法链表
        struct objc_cache *cache;	//方法缓存
        struct objc_protocol_list *protocols;	//协议链表
    #endif
    
    } OBJC2_UNAVAILABLE;
    

    从上面的objc_class的定义里面,看到了作为一个Class的类,需要哪些东西,再详细表述下其中具体重要的概念:

    isa:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类),我们会在后面介绍它。

    super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为nil。

    cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。

    version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它让我们识别出不同类定义版本中实例变量布局的改变。

    其中值得我们关注的是isa指向的元类。

    什么是元类?

    元类是一个类对象的类。进一步的解释:当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。元类存储着一个类的所有类方法。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。元类也是类,可以对它发消息,那么它的isa指向哪里?

    为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。这样就形成了一个完美的闭环。再联想下一些类的静态方法,当我们调用[NSObject alloc]时,其实走的是meta元类查找alloc方法的逻辑。

    上一张网络通用神图,来把上面的大段文字转化表达下:


    继续深挖objc_class里面的其他东西:

    成员变量链表objc_ivar_list

    开始挖:

    struct objc_ivar_list {
        int ivar_count;
    #ifdef __LP64__
        int space;
    #endif
        /* variable length structure */
        struct objc_ivar ivar_list[1];
    };
    

    发现objc_ivar结构体,继续挖:

    struct objc_ivar {
        char *ivar_name;	//成员变量名称
        char *ivar_type;	//成员变量类型
        int ivar_offset;	//偏移量
    #ifdef __LP64__
        int space;
    #endif
    }
    

    其中objc_ivar_list结构体存储着objc_ivar数组列表,而objc_ivar结构体存储了类的单个成员变量的信息。注意第三个成员ivar_offset。它表示基地址偏移字节。Runtime会进行检测来调整类中新增的变量的偏移量。 这样就可以通过【对象地址 + 基类大小 + 变量偏移字节】来计算出变量相应的地址,并访问到相应的变量。

    方法链表objc_method_list
    struct objc_method_list {
        struct objc_method_list *obsolete;
        int method_count;
    #ifdef __LP64__
        int space;
    #endif
        /* variable length structure */
        struct objc_method method_list[1];
    }
    

    发现objc_method结构体,继续挖:

    struct objc_method {
        SEL method_name;	//方法名称
        char *method_types;	//方法类型,存储着方法的参数类型和返回值类型。
        IMP method_imp;	//方法指针,本质上是一个函数指针
    }
    

    SEL我们上一篇说过,selector标识,代表唯一仅有的一个方法。

    IMP继续深挖:typedef id (*IMP)(id, SEL, ...);

    IMP是一个函数指针,这是由编译器生成的。当你发起一个Objc消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。IMP 这个函数指针就指向了这个方法的实现。这个函数指针和objc_msgSend函数完全一致的定义。

    看完objc_method后,可以得知objc_class中objc_method_list保存了一组SEL与IMP的映射。通过查找SEL我们可以找到方法执行的入口IMP,然后调用IMP去执行对应的方法。

    objc_cache

    objc_cache用来缓存用过的方法,提高性能。objc_msgSend每调用一次方法后,就会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。

    协议链表objc_protocol_list

    存放需要遵循的协议链表。与方法链表类似。


    最后总结下如何发送消息,举objc_msgSend(receiver, message)这个例子来说:

    1.检查Message的SEL是否需要忽略。比如Mac OSX开发,有了垃圾回收就不理会retain,release这些函数了。

    2.检查receiver是否为nil。ObjC的特性是允许对一个nil对象执行任何一个方法不会Crash,因为会被忽略掉。

    3.通过receiver的isa指针找到它的Class,然后根据SEL去找IMP;

    4.首先在objc_cache缓存中去找message的SEL,如果找到,则调用对应的IMP。否则继续下一步。

    5.在Class的objc_method_list找message的SEL;

    6.如果Class中没有找到message的SEL,继续往它的super_class中找,直到找到基类NSObject为止;

    7.一旦找到message这个函数的SEL,就去执行SEL对应的实现IMP;

    8.这时会使用到objc_cache缓存,把常用的函数都存放到缓存中来,提高调用的命中率。

    9.如果一直找到基类,仍然没有找到message的SEL,进入动态方法解析和消息转发的机制。

    继续留下了线索。什么是动态方法解析和消息转发的机制?篇幅已经很长,不宜展开继续记录学习。

  • 相关阅读:
    阅读笔记7
    阅读笔记6
    架构阅读笔记5
    软件质量属性——易用性课堂讨论问题总结
    Git 的 .gitignore 配置
    zookeeper的简单搭建,java使用zk的例子和一些坑
    MySQL中有关TIMESTAMP和DATETIME的对比
    Mysql 如何设置字段自动获取当前时间,附带添加字段和修改字段的例子
    spring boot注入error,Consider defining a bean of type 'xxx' in your configuration问题解决方案
    net start命令发生系统错误5和错误1058的解决方法
  • 原文地址:https://www.cnblogs.com/vokie/p/9282819.html
Copyright © 2011-2022 走看看