场景
目前项目中RN模块已经改造成了拆包方式,每次在初始化的时候先加载common代码,然后进入相具体业务页面加载business代码,虽然business的代码只有几十k左右,但是没有预加载的情况下,等待加载完毕也需要一些时间,虽然是瞬间的,用户还是能感受到页面白屏情况;而且,在用户关闭页面后再次打开相同页面是需要重新创建RCTBridge的,没有复用之前的RCTBridge实例;因此,不妨考虑下使用缓存管理的模式,将已经加载完的RCTBridge缓存起来,供相同模块所在的页面复用,用户在二次打开的时候没有任何白屏感知,和原生交互完全一致。
缓存带来的问题
- RCTBridge实例在加载完common+business代码后会占用2M内存,如果实例保存太多,将会出现内存OOM
- 因为是复用RCTBridge实例的,js代码需要避免使用全局变量,因为一旦二次进入页面已经改变的值是不会重置的
缓存策略
使用hashtable+双向链表来存储RCTBridge实例,count或cost超出阈值,会按照LRU策略清理没有使用的RCTBridge实例;LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象,提升缓存命中;我们先来看下LRU策略运作方式:
LRU缓存实现类似于一个特殊的栈,把访问过的元素放置到栈顶(若栈中存在,则更新至栈顶;若栈中不存在则直接入栈),然后如果栈中元素数量超过限定值,则删除栈底元素(即最近最少使用的元素)
存储结构
使用hashtable可以在在时间复杂度O(1)内完成数据访问;使用双向链表实现,可以在时间复杂度O(1)内完成删除和插入的操作;保存方式如下:
key和value
- key: businessURL + commonURL
- value: LinkedNode
按照拆包的模式下,common包和business包会分别进行codepush热更新检查,详见codepush支持多bundle更新重构,那么安装完毕以后两者其中之一的bundle路径有可能改变,那么原来通过缓存存储的RCTBridge和现有common+business加载完成的RCTBridge是有差异的,不能单纯的只靠bundle名来作为key值,而必须通过businessURL + commonURL成对的bundle路径作为key值。
使用LinkedNode作为双向链表的数据载体,存储前后关系,头部数据prev和尾部数据的next值为空;具体结构如下:
@interface LinkedNode : NSObject
@property (nonatomic, copy) NSString* key;
@property (nonatomic, Strong) RCTBridge* value;
@property (nonatomic, Strong) LinkedNode* next;
@property (nonatomic, Strong) LinkedNode* prev;
@end
实际情况分析
传统的memryCache在存入新的数据时,count或cost超出阈值的情况下使用LRU淘汰策略,如果对象释放不需要再额外执行invalidate等代码,一般都是由系统自带的GC自动处理内存,就算有别的控制器正在使用也不会立即释放,只有当对象处于无引用状态下才会参与释放流程;然而在我们的拆包项目中,RCTBridge实例被创建并使用后,内部创建了一些计时器等模块造成循环引用,需要执行invalidate才能断开释放内存,如果某个控制器正在使用RCTBridge实例,且刚好出现LRU淘汰策略执行了invalidate,那么该控制器就会出现意想不到的后果,所以必须手动对RCTBridge实例的使用情况进行计算,确保没有控制器使用的情况下才会参与LRU淘汰策略;
既然是手动对RCTBridge实例的使用情况进行计算,那么执行LRU淘汰策略的时机改成当某个实例使用次数变成0的情况下执行更加合理,试想,每次存入新的实例情况下,其他实例也处于正在使用状态就算执行淘汰策略也是浪费;
实现流程
(1)每次存储的时候将当前LinkedNode记录在栈顶,记录最后一个LinkedNode;
(2)当某个RCTBridge实例使用次数变成0的情况下触发检查缓存使用情况,从最后一个LinkedNode向前遍历,一旦找到未使用的RCTBridge实例将其清理且调用invalidate;
(3)当执行LRU淘汰策略以后,需要对删除的LinkedNode前后两个LinkedNode重新创建链接关系,如果删除的是最后一个LinkedNode需要将前一个LinkedNode作为最后一个标识。
LRU算法注意要点
- 在存入数据的时候,发现key已经存在,直接获取到LinkedNode将其记录在栈顶,放置到栈顶以后要将prev清空
- 如发现需要置顶的LinkedNode原先处于栈尾,将LinkedNode的prev数据作为新的栈尾
- 双向链表每次重新建立链接关系,需要考虑线程安全问题,通过加锁的方式解决