二、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,进入动态方法解析和消息转发的机制。

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

猜你喜欢

转载自www.cnblogs.com/vokie/p/9282819.html