Skip to content

Latest commit

 

History

History
213 lines (118 loc) · 7 KB

2、方法缓存.md

File metadata and controls

213 lines (118 loc) · 7 KB

方法缓存

我们先来整体的看一下结构

方法缓存

  • 1、class类中只要有isa指针superClasscache方法缓存bits具体的类信息
  • 2、bits & FAST_DATA_MASK 指向一个新的结构体Class_rw_t,里面包含着methods方法列表properties属性列表protocols协议列表class_ro_t类的初始化信息等一些类信息

Class_rw_t Class_rw_t里面的methods方法列表properties属性列表都是二维数组,是可读可写的,包含类的初始内容分类的内容

方法缓存1

class_ro_t

class_ro_t里面的baseMethodList,baseProtocols,Ivars,baseProperties是一维数组,是只读的,包含类的初始化内容

方法缓存2

method_t

method_t是对方法的封装

struct method_t{
	SEL name;//函数名
	const char *types;//编码(返回值类型,参数类型)
	IMP imp;//指向函数的指针(函数地址)
}

IMP

IMP代表函数的具体实现

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector)

SEL

SEL代表方法名,一般叫做选择器,底层结构跟char *类似

  • 可以通过@selector()sel_registerName()获得
  • 可以通过sel_getName()NSStringFromSelector()转成字符串
  • 不同类中相同名字的方法,所对应的方法的选择器是相同的
  • 具体实现typedef struct objc_selector *SEL

types

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

结构为:返回值 参数1 参数2...参数N

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

方法缓存3

例如

// "i24@0:8i16f20"
// 0id 8SEL 16int 20float  == 24
- (int)test:(int)age height:(float)height

每一个方法都有两个默认参数self_msg 我们可以查到id类型为@SEL类型为:

  • 1、第一个参数i返回值
  • 2、第二个参数@id 类型的self
  • 3、第三个参数:SEL 类型的_msg
  • 4、第四个参数iInt age
  • 5、第五个参数ffloat height

其中加载的数字其实是跟所占字节有关

  • 1、24 总共占有多少字节
  • 2、@0id 类型的self的起始位置为0
  • 3、:8 是因为id 类型的self占字节为8,所以SEL 类型的_msg`的起始位置为8

方法缓存

Class内部结构中有一个方法缓存cache_t,用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度。

方法缓存4

cache_t结构体里面有三个元素

  • buckets 散列表,是一个数组,数组里面的每一个元素就是一个bucket_t,bucket_t里面存放两个

    • _key SEL作为key
    • _imp 函数的内存地址
  • _mask 散列表的长度

  • _occupied已经缓存的方法数量

为什么会用到方法缓存

OC对象的分类2

这张图片是我们方法产找路径,如果我们的一个类有多个父类,需要调用父类方法,他的查找路径为

  • 1、先遍历自己所有的方法
  • 2、如果在自己类中找不到方法,则遍历父类所有方法,没有查找到调用方法之前,一直重复该动作 如果每一次方法调用都是走这样的步骤,对于系统级方法来说,其实还是比较消耗资源的,为了应对这个情况。出现了方法缓存,调用过的方法,都放在缓存列表中,下次查找方法的时候,现在缓存中查找,如果缓存中查找不到,然后在执行上面的方法查找流程。

散列表结构

方法缓存5

散列表的结构大概就像上面那样,数组的下标是通过@selector(方法名)&_mask来求得,具体每一个数组的元素是一个结构体,里面包含两个元素_imp@selector(方法名)作为的key

我们在上一篇文章中知道,一个值与&上一个_mask,得出的结果一定小于等于_mask值,而_mask值为数组长度-1,所以任何时候,也不会越界。

其实这就是散列表的算法,也有一些其他的算法,取余,一个值取余&的效果是相同的。

但是这其实是有几个疑虑的

  • 1、初始_mask是多少? - 初始_mask我简单了尝试了一下,第一次可能给3
  • 2、随着方法的增加,方法数量超过_mask值了怎么办 - 随着方法的增多,方法数量肯定会超过_mask,这个时候会清空缓存散列表,然后_mask*2
  • 3、如果两个值&_mask的值相同了怎么办 - 如果两个值&_mask的值相同时,第二个&减一,知道找到空值,如果减到0还没有找到空位置,那就放在最大位置
  • 4、在没有存放cach_t的数组位置怎么处理
    • 在没有占用是,会在空位置的值为NULL

源码查看 我们在objc-cache.mm文件中查找bucket_t * cache_t::find(cache_key_t k, id receiver)方法。

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
	assert(k != 0);

	bucket_t *b = buckets();
	mask_t m = mask();
	mask_t begin = cache_hash(k, m);
	mask_t i = begin;
	do {
	if (b[i].key() == 0  ||  b[i].key() == k) {
		return &b[i];
		}
	} while ((i = cache_next(i, m)) != begin);

	// hack
	Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
	cache_t::bad_cache(receiver, (SEL)k, cls);
}

计算index值

mask_t begin = cache_hash(k, m);

这个方式是计算下标的,我们点击进入查看具体实现,就是@selector(方法名)&_mask

方法缓存6

当两个值求的下标相同时

(i = cache_next(i, m)) != begin

具体实现为

方法缓存7

arm64x86实现方法不一样

这里有一个MJ老师封装的能够查看对象各种属性的方法,想要使用的可以在这里查看

方法缓存8