这篇文章,我们主要来介绍一下objc_class结构的内容与含义。
我们知道Class的类型是objc_class类型
typedef struct objc_class *Class;
点进去objc_class可以看到部分定义:
objc_class继承objc_object
objc_object的部分定义:
简化可归结objc_class的结构为:
可以看出:
Class里面有isa、superclass指针,方法列表、属性列表、协议列表以及成员变量列表以及其他信息。
其中,方法列表包含了自己的方法列表以及分类的方法列表,且分类方法列表在原类方法列表前面。
通过操作objc_class里面的bits与上一个定义好的数值,就可以找到class_rw_t里面的内容。这种做法我们在上一篇文章中已经讲解过,用到的是位运算操作,这里不再做叙述。
其中:
class_rw_t中的rw是read和write的意思,也就是class_rw_t里面的内容是可读可写的。
class_ro_t中的ro是read和only read的意思,也就是class_ro_t里面的内容是只读的。
通过iOS大神MJ老师写的文件MJClassInfo.h,我们可以窥探class的具体内容。
导入文件,注意,Add to targets不要选中
YZPerson *person = [[YZPerson alloc] init];
mj_objc_class *cls = (__bridge struct mj_objc_class *)([YZPerson class]);
class_rw_t *data = cls->data();
好强大的工具,给MJ点赞
在class_rw_t存储的方法列表、属性列表、协议列表都是二维数组。
以方法列表为例:
method_array_t里面是method_list_t;method_list_t里面是结构体method_t
struct method_t {
SEL name;
const char *types;
IMP imp;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
在源码中可以看出:
即
而class_ro_t里面也有方法列表
参考源码,可以得出:
原始的class_ro_t中的方法列表与分类中的方法列表结合,放到class_rw_t的方法列表中。
更多学习:
Runtime源代码解读4(方法列表)
这是个大佬
method_t分析
method_t是对方法/函数的封装。
通过查看源码,可以看到method_t结构的组成:
struct method_t {
SEL name;//函数名
const char *types;//编码(返回值类型、参数类型)
IMP imp;//指向函数的指针(函数地址)
};
其中,IMP代表函数的具体实现,imp是指向函数地址的指针,通过IMP可以真正的找到函数的入口。
SEL代表方法/函数名,一般叫做选择器,底层结构跟char *类似。
可以通过@selector()和sel_registerName()获得
可以通过sel_getName()和NSStringFromSelector()转成字符串
不同类中相同名字的方法,所对应的方法选择器是相同的
types是一个包含了函数的返回值类型、参数类型的编码字符串
其属于Type Encoding类型编码
iOS提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码。
对应含义可以在官方文档查看,这里仅列出一部分:
我们需要了解的是:
可以通过method_t里面的types获取到函数的返回值类型以及参数类型。
方法缓存
在objc_class里面有一个cache_t类型的cache,用散列表来缓存曾经调用过的方法,可以提高方法的查找速度。
其中,cache_t的组成为:
struct cache_t {
struct bucket_t *_buckets;//散列表
mask_t _mask;//散列长度-1
mask_t _occupied;//已经缓存的方法数量
}
struct bucket_t {
cache_key_t _key;//SEL作为key
IMP _imp;//函数的内存地址
};
可以看出,用方法名作为key,找函数内存地址,形成一个散列表。
举个具体例子分析:
YZPerson *person = [[YZPerson alloc] init];
[person run];
方法缓存过程:
第一次调用某个对象方法的时候,会去该类的类对象cache中通过key-value方法查找,key是方法名,查找对应的IMP。
如果查找不到,会通过isa指针去其类对象里面查找该方法,有的话就通过散列表缓存到cache中并且调用该方法。
如果该类对象里面没有该方法,则去其父类里面找,也是先去缓存列表中找,找到就放入到自己的缓存中并调用。缓存中没有则去方法列表中找,找到会通过散列表缓存到自己cache中并且调用,没找到会继续去父类中找,直到找不到。第二次调用,去cache中查找,有,直接调用,没有继续按上面的方法做。