1、Class的结构

image.png
class结构中包含了isa共用体、superclass指针、方法缓存和bits,由位运算知识点可知,bits保存了Class的一些信息,通过bits&FAST_DATA_MA可以获取累的class_rw_t信息。
class_rw_t包含了方法列表、属性列表、协议列表等信息,还包含了一个class_ro_t结构体,里面保存了class的类名、成员变量等初始信息。

*中的objc_class结构体已经过时,没有参考价值,需要下载最新的objc源码查看。

2、class_rw_t的结构

image.png

实际上class_rw_t内部保存的是metho_array_t、property_array_t、protocol_array_t类型,和metho_list_t意义相同,是二维数组。

class_rw_t内部的methods、properties、protocols都是二维数组,是可读可写的,包含了类的初始内容、分类的内容。
比如methods内部保存了metho_list_t对象,method_list_t内部保存了真正的方法_method_t,类初始方法列列表会排在最后,后编译的分类中的方法会保存在methods前面。

3、class_ro_t的结构

class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容。
image.png

4、metho_t的结构

  1. struct method_t {
  2. SEL name; // 函数名
  3. const char *types; // 编码(返回值类型、参数类型)
  4. IMP imp; //指向函数的指针()
  5. };

IMP代表函数的具体实现

SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似
可以通过@selector()和sel_registerName()获得
可以通过sel_getName()和NSStringFromSelector()转成字符串
不同类中相同名字的方法,所对应的方法选择器是相同的

types包含了函数返回值、参数编码的字符串

5、Type Encoding

iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码,可以通过打印查看:

  1. NSLog(@"@encode(int) = %s", @encode(int));
  2. NSLog(@"@encode(void) = %s", @encode(void));
  3. NSLog(@"@encode(id) = %s", @encode(id));
  4. NSLog(@"@encode(SEL) = %s", @encode(SEL));
  1. ~: @encode(int) = i
  2. ~: @encode(int) = v
  3. ~: @encode(int) = @
  4. ~: @encode(int) = :

比如下面两个方法,test方法对应metho_t中的types值就是v16@0:8,testAge:height:方法对应metho_t中的types值就是v24@0:8i16i20。

  1. // v16@0:8
  2. - (void)test;
  3. // v24@0:8i16i20
  4. - (void)testAge:(int)age height:(int)height;

test方法默认会带有两个参数:

  1. - (void)testSelf:(id)self _cmd:(SEL)_cmd;

image.png

*也可以简写成v@:

6、方法缓存cache_t

cache_t是用哈希表来缓存曾经调用过的方法,可以提高方法的查找速度,避免多次执行方法查找逻辑。

  1. struct cache_t {
  2. bucket_t *_buckets; // 哈希表
  3. mask_t _mask; // _buckets长度-1
  4. mask_t _occupied; // 已缓存的方法数量
  5. };
  6. struct bucket_t {
  7. cache_key_t _key; // sel作为key
  8. IMP _imp; // 函数内存地址
  9. };

*例如哈希表_buckets初始长度是10,那么_mask是9,_occupied可能是3。在查找方法时,通过sel(方法名)作为key,找到对应的bucket_t,从而找到_imp(实现)进行调用。

方法写入和读取的过程如下图所示:
image.png
比如调用了方法@selector(test),需要先计算出哈希索引,再将bucket_t插入到索引位置/从索引位置读取,_buckets其他位置如果没有信息会留空,是以空间换时间的方案。
_buckets哈希索引计算方法:sel & mask

  1. static inline mask_t cache_hash(SEL sel, mask_t mask)
  2. {
  3. uintptr_t value = (uintptr_t)sel;
  4. #if CONFIG_USE_PREOPT_CACHES
  5. value ^= value >> 7;
  6. #endif
  7. return (mask_t)(value & mask);
  8. }

如果不同的方法通计算出来的索引值相同,就需要解决哈希冲突,苹果的方法时让索引-1:

  1. static inline mask_t cache_next(mask_t i, mask_t mask) {
  2. return i ? i-1 : mask;
  3. }

写入:计算出来的索引位置已经保存了bucket_t,则让索引-1,如果还不行继续-1,索引小于0时,将索引设置成mask(_buckets长度-1)从最后的位置继续向前遍历,直到找到空位置,进行插入操作。

读取:计算出来的索引位置对应的bucket_t中的SEL如果和传入的SEL不一致,就让索引-1查找,如果还不相同,则继续-1,索引小于0时将索引设置成mask(_buckets长度-1),从最后位置继续向前遍历,直到找到SEL相同的bucket_t。

扩容:当_buckets空间不够时会进行扩容操作(原有空间大小*2),会更新mask的值,并且清空_buckets。

7、方法查找

isa、superclass 中说明了OC方法的调用轨迹,这里做一下补充,在查找过程中需要先查找方法缓存,过程如下:
image.png