iOS底层原理篇(二)----类的缓存

本文深入探讨iOS Runtime中类的缓存机制,解析cache_t结构体如何存储方法的_imp和_key,以及缓存填充、扩容和哈希算法在动态调整缓存大小中的作用。

接上篇文章:类的本质和底层实现,我们今天说一下类的缓存!

  • 先看一下上篇中类的结构图:
    类的结构体
  • 在图中,我们看到 cache_t cache ,这是一个结构体,就是我们要说的缓存!那么我们的类在缓存里面存了些什么东西呢?

接下来我们探索一下

  • 当然先来看一看cache_t结构:
    cache_t结构
  • 在 cache_t 中有两种类型, bucket_t 和 mask_t ,我们在看一下这两个是怎么定义的:
    mask_t和bucket_t结构
    method_t结构
    IMP
  • 从上面的图中,我们看到,根据架构,不同里面存的是_imp和_key,这里面缓存的应该是方法!
  • iOS Runtime详解—源码分析中,我们知道,消息发送过程中,首先调用的是快速查找的方法,就是从缓存中去找sel对应的imp,找到直接返回!另外一种方法,就是从类、父类、元类、根元类、再到NSObject中查找sel对应的imp,找到之后进行cache_fill,也就是填充到缓存中的操作,并且返回imp!如果这些都找不到就会进行动态方法决议进行消息转发的流程!

下面我们来验证一下cache_t是不是缓存了这些方法:

int main(int argc, const char * argv[]) {
	@autoreleasepool {
    	WMPerson *person = [[WMPerson alloc] init];
    	Class pClass = [WMPerson class];
    	[person look];
    	[person say];
    	[person walk];
    
    	[person run];
    	[WMPerson jump];//类方法
    	NSLog(@"%@ - %p",person,pClass);
	}
	return 0;
}
/*
断点在[person walk];然后我们lldb打印:
*/
(lldb) x/4gx pClass
0x100002498: 0x001d800100002471 0x0000000100b37140
0x1000024a8: 0x0000000100ff9bf0 0x0000000300000003
(lldb) p (cache_t *)0x1000024a8    //0x1000024a8 cache_t的内存首地址
(cache_t *) $1 = 0x00000001000024a8
(lldb) p *$1
(cache_t) $2 = {
	_buckets = 0x0000000100ff9bf0
	_mask = 3
	_occupied = 3
}
(lldb) p $2._buckets
(bucket_t *) $3 = 0x0000000100ff9bf0
(lldb) p *$3
(bucket_t) $4 = {
	_key = 140734683087864
	_imp = 0x00000001000019a0 (LGTest`-[WMPerson look] at WMPerson.m:12)
}
/*
我们打印到bucket_t获取到了缓存
(bucket_t) $4 = {
	_key = 140734683087864
	_imp = 0x00000001000019a0 (LGTest`-[WMPerson look] at WMPerson.m:12)
}
但是还是没有看到其他方法的缓存!
断点走到[person run]所在行
重新进行打印
*/
(lldb) p (cache_t *)0x1000024a8
(cache_t *) $6 = 0x00000001000024a8
(lldb) p *$6
(cache_t) $7 = {
	_buckets = 0x0000000102200270
	_mask = 7
	_occupied = 1
}
(lldb) p $7._buckets
(bucket_t *) $8 = 0x0000000102200270
(lldb) p *$8
(bucket_t) $9 = {
_key = 0
_imp = 0x0000000000000000
}

/*
我们看到结果:
cache_t结构体中只有改变_mask : 3 -> 7 _occupied : 3 -> 1
(cache_t) $7 = {
	_buckets = 0x0000000102200270
	_mask = 7
	_occupied = 1
}
但是打印的bucket_t:
(bucket_t) $9 = {
_key = 0
_imp = 0x0000000000000000
}
变为了空!
*/

我们知道了我们的类在调用方法之后是做了缓存的,但是具体的一个缓存的原理我们还是不知道的,那么要知道原理,就要先知道有关cache缓存的一个流程,我们搜索cache慢慢查找(比较笨,因为我也不知道在哪)或者通过汇编
cache相关
cache_fill缓存填充

  • 在这个方法处添加断点,运行objc项目,我们确实猜的没错,程序被断在这里!

      void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
      {
      //首先来到这个方法cache_fill缓存填充
      #if !DEBUG_TASK_THREADS  //这个宏定义调试使用
      	mutex_locker_t lock(cacheUpdateLock); //跳转到下面的方法进行缓存的设置
      	cache_fill_nolock(cls, sel, imp, receiver);
      #else
      	_collecting_in_critical();
      	return;
      #endif
      }
    
      static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
      {
      	cacheUpdateLock.assertLocked();
      	
      	// Never cache before +initialize is done
      	if (!cls->isInitialized()) return;//如果cls不存在,那就没有cache缓存,就没必要在查找,直接返回
    
      	// Make sure the entry wasn't added to the cache by some other thread 
      	// before we grabbed the cacheUpdateLock.
      	//确保在获取cacheUpdateLock之前,imp没有被其他线程添加到缓存中.
      	//再次检查获取缓存
      	if (cache_getImp(cls, sel)) return;
      	//根据cls获取对应的缓存cache_t数组
      	cache_t *cache = getCache(cls);
      	cache_key_t key = getKey(sel);//cache_key_t类型为unsigned long
    
      	// Use the cache as-is if it is less than 3/4 full
      	//如果缓存不足3/4,则按原样使用缓存
      	//occupied() + 1 这里加一,是因为系统没有防止内存溢出
      	//没有将缓存大小设置到100%,在赋值的时候进行了减一!
      	mask_t newOccupied = cache->occupied() + 1;
      	mask_t capacity = cache->capacity();
      	if (cache->isConstantEmptyCache()) {
      		// 如果cache是空,则给初始化值4
      		//INIT_CACHE_SIZE : 4
      		//enum {
      		//INIT_CACHE_SIZE_LOG2 = 2,
      		//INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
      		//};
      		cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
      	} else if (newOccupied <= capacity / 4 * 3) {
      		// Cache is less than 3/4 full. Use it as-is.
      		//缓存小于3/4,不进行扩容.
      	} else {
      		// Cache is too full. Expand it.
      		//缓存已满,扩容!
      		cache->expand();
      	}
      	// Scan for the first unused slot and insert there.
      	// There is guaranteed to be an empty slot because the 
      	// minimum size is 4 and we resized at 3/4 full.
      	//这里说明此时插入缓存是肯定不会内存溢出的
      	//因为我们判断的是申请内存大小的3/4
      	//此时缓存扩容前的调用方法,之前缓存的方法已经被清空
      	//缓存里面只有一个方法
      	bucket_t *bucket = cache->find(key, receiver);
      	if (bucket->key() == 0) cache->incrementOccupied();
      	bucket->set(key, imp);
      }
      
      mask_t cache_t::capacity() 
      {
      	return mask() ? mask()+1 : 0; 
      }
    
      void cache_t::expand() 
      {
      	cacheUpdateLock.assertLocked();
      	uint32_t oldCapacity = capacity();//获取旧的缓存大小
      	//缓存扩大到原来的两倍!!!!
      	uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    
      	if ((uint32_t)(mask_t)newCapacity != newCapacity) {
      		// mask overflow - can't grow further
      		// fixme this wastes one bit of mask
      		newCapacity = oldCapacity;
      	}
      	reallocate(oldCapacity, newCapacity);
      }
      
      /*
      扩容设置
      */
      void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
      {
      	//扩容时,若果cache不是空:
      	//bool cache_t::canBeFreed(){return !isConstantEmptyCache();}
      	//则需要释放旧的缓存
      	bool freeOld = canBeFreed();
    
      	bucket_t *oldBuckets = buckets();
      	bucket_t *newBuckets = allocateBuckets(newCapacity);//创建新的bucket
      	
      	assert(newCapacity > 0);
      	assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    
      	//newCapacity - 1是为了防止大小超过百分之百
      	//我们看到了这里的减1!
      	setBucketsAndMask(newBuckets, newCapacity - 1);
      	
      	//释放旧的缓存
      	if (freeOld) {
      		cache_collect_free(oldBuckets, oldCapacity);
      		cache_collect(false);
      	}
      }
    
      bucket_t * cache_t::find(cache_key_t k, id receiver)
      {
      	assert(k != 0);
    
      	bucket_t *b = buckets();//获取buckets数组
      	mask_t m = mask();
      	// 通过cache_hash哈希函数和参数cache_key_t&mask_t
      	//计算出开始查找的位置,
      	//begin用来记录查询起始索引
      	mask_t begin = cache_hash(k, m);
      	// begin 赋值给 i,用于切换索引
      	mask_t i = begin;
      	do {
      		 if (b[i].key() == 0 || b[i].key() == k) {
          	 	//用索引i从散列表取值
          	 	//如果取出来的bucket_t的key = k,则查询成功,返回该bucket_t,
          	 	//如果key = 0,说明在索引i的位置上还没有缓存过方法
          	 	//同样需要返回该bucket_t,用于中止缓存查询.
          	 	return &b[i];
      		 }
      	} while ((i = cache_next(i, m)) != begin);
      	// while条件判断相当于 i = i-1,回到上面do循环里面
      	//相当于查找散列表上一个单元格里面的元素,再次进行key值 k的比较,
      	//当i=0时,也就是i指向散列表最首个元素索引的时候重新将mask赋值给i,使其指向散列表最后一个元素,重新开始反向遍历散列表,
      	//其实就相当于绕圈,把散列表头尾连起来
      	//从begin值开始,递减索引值,当走过一圈之后,必然会重新回到begin值
      	//如果此时还没有找到key对应的bucket_t,或者是空的bucket_t,则循环结束
      	//说明查找失败,调用bad_cache方法.
      	
      	Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
      	cache_t::bad_cache(receiver, (SEL)k, cls);
      }
      /**
      上面步骤中,我们如果没有缓存,首先开辟缓存控件,开辟缓存控件大小为4
      当我们调用方法过程中,如果缓存内容大于缓存的3/4,就会开启扩容:expand()
      将内存容量扩大为原来的2倍,并且将之前的缓存清空(为了快)
      当扩容操作完成后,会进行一项操作,就是将扩容前调用的方法存到缓存中
      这个存储的顺序并不是按照数组那样顺序存储,而是根据哈希算法生成的索引进行缓存
      */
    

下面我们通过代码断点去验证这个过程:
内存扩容

  • 打印:

      (lldb) x/4gx pClass
      0x1000013f8: 0x001d8001000013d1 0x0000000100b36140
      0x100001408: 0x0000000100f502a0 0x0000000100000007
      (lldb) p (cache_t *)0x100001408
      (cache_t *) $1 = 0x0000000100001408
      (lldb) p *$1
      (cache_t) $2 = {
      _buckets = 0x0000000100f502a0
      _mask = 7
      _occupied = 1
      }
      (lldb) p $2._buckets
      (bucket_t *) $4 = 0x0000000100f502a0
      (lldb) p $4[0] //正好存在了索引0的位置
      (bucket_t) $6 = {
      	_key = 4294971016
      	_imp = 0x0000000100000b10 (LGTest`-[WMPerson walk] at WMPerson.m:20)
      }
      (lldb) p $4[1] //下面1-7均为空,之前的缓存被清空,扩容之后只缓存了walk方法,其他均为0
      (bucket_t) $5 = {
      	_key = 0
      	_imp = 0x0000000000000000
      }
      (lldb) p $4[2]
      (bucket_t) $7 = {
      	_key = 0
      	_imp = 0x0000000000000000
      }
      (lldb) p $4[3]
      (bucket_t) $8 = {
      	_key = 0
      	_imp = 0x0000000000000000
      }
      (lldb) p $4[4]
      (bucket_t) $9 = {
      	_key = 0
      	_imp = 0x0000000000000000
      }
      (lldb) p $4[5]
      (bucket_t) $10 = {
      	_key = 0
      	_imp = 0x0000000000000000
      }
      (lldb) p $4[6]
      (bucket_t) $11 = {
      	_key = 0
      	_imp = 0x0000000000000000
      }
      (lldb) p $4[7]
      (bucket_t) $12 = {
      	_key = 0
      	_imp = 0x0000000000000000
      }
      (lldb) p $4[8] //此处打印野指针了
      (bucket_t) $13 = {
      	_key = 1
      	_imp = 0x0000000100f502a0 (0x0000000100f502a0)
      }
    

类的缓存缓存了调用过程中的一些方法,并且在缓存过程中动态的改变缓存大小,并清空扩容前的缓存数据(为了快,如果保存之前的,需要哈希值的一个映射过程,比较慢),缓存最新调用的方法!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值