我们接下来看看类对象的本质,其实就是下面这个结构体:
struct objc_class : objc_object {
Class isa;//这个isa指针本来是在objc_object里面的,现在把它拿上来这里
Class superclass;//指向父类的指针
cache_t cache; // 方法缓存
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
最后一个bits存储着非常多的东西,跟之前说的位域一样,想要取出某些东西必须bits&XX_MASK掩码。比如我们要取出这个类对象里面存储的data数据class_rw_t,则必须bits&FAST_DATA_MASK得到class_rw_t。
struct class_rw_t {//re=read write可读写的
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;//ro=read only是只读的,无法修改的
method_array_t methods;//方法列表,如果是元类对象,那么放的就是类方法
property_array_t properties;//属性列表
protocol_array_t protocols;//协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
这个class_rw_t是可读写的,我们runtime运行时可以动态读写里面的信息,比如增加协议,方法,属性等。
class_ro_t的结构如下:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;//instance对象占用的内存空间
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;//类名
method_list_t * baseMethodList;//方法列表
protocol_list_t * baseProtocols;//协议列表
const ivar_list_t * ivars;//成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容。
比如methods里面存放的是method_list_t数组,method_list_t数组里面装的才是method_t
class_ro_t里面存放的baseMethodList、baseProtocols、ivars是类里面本身初始化的方法、协议和属性不包括分类的,这些是一维数组,是不可更改的。
当我们的程序运行起来的时候系统会将class_ro_t里面的方法生成一个method_list_t数组放到class_rw_t的methods里面,并且分类里面的方法数组也会生成一个method_list_t合并到class_rw_t的methods里面,我们运行时动态修改方法就是只能修改class_rw_t里面的方法。
iOS方法的内部实现实际上是这样:
struct method_t {
SEL name;//函数名
const char *types;//编码(返回值类型、参数类型)
IMP imp;//指向函数的指针(函数地址)
}
SEL name函数名其实就是@selector(test),说白了这个就是函数名字符串
const char *types编码包含了函数的返回值类型和参数类型,比如以下test方法:
- (void) test{}
它的types是v16@0:8,v表示返回值类型是void,@表示id类型,:代表SEL类型,每个OC方法底层默认都会自带两个参数-(void)test:(id)self _cmd:(SEL)_cmd{}
方法缓存 cache_t cache
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即@selector(test)
IMP _imp;//函数的内存地址
}
当我们在散列表找方法时,我们拿@selector(test) & _mask得到对应的索引,然后拿函数地址与对应索引的函数地址比较,如果不对,那么_mask减1,如果_mask为0,则_mask就赋值为散列表的长度继续找。如果散列表扩容了,那么编译器会清空缓存,即清空散列表,然后再重新存取。
OC方法执行流程:
OC调用一个方法实质就是通过objc_msgSend方法向这个对象发送一条消息。主要有三大流程:消息查找发送、动态方法解析、消息转发
消息发送
动态方法解析
消息转发