二、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,进入动态方法解析和消息转发的机制。
继续留下了线索。什么是动态方法解析和消息转发的机制?篇幅已经很长,不宜展开继续记录学习。