場(chǎng)景
目前項(xiàng)目中RN模塊已經(jīng)改造成了拆包方式,每次在初始化的時(shí)候先加載common代碼,然后進(jìn)入相具體業(yè)務(wù)頁面加載business代碼,雖然business的代碼只有幾十k左右,但是沒有預(yù)加載的情況下,等待加載完畢也需要一些時(shí)間,雖然是瞬間的,用戶還是能感受到頁面白屏情況;而且,在用戶關(guān)閉頁面后再次打開相同頁面是需要重新創(chuàng)建RCTBridge的,沒有復(fù)用之前的RCTBridge實(shí)例;因此,不妨考慮下使用緩存管理的模式,將已經(jīng)加載完的RCTBridge緩存起來,供相同模塊所在的頁面復(fù)用,用戶在二次打開的時(shí)候沒有任何白屏感知,和原生交互完全一致。
緩存帶來的問題
- RCTBridge實(shí)例在加載完common+business代碼后會(huì)占用2M內(nèi)存,如果實(shí)例保存太多,將會(huì)出現(xiàn)內(nèi)存OOM
- 因?yàn)槭菑?fù)用RCTBridge實(shí)例的,js代碼需要避免使用全局變量,因?yàn)橐坏┒芜M(jìn)入頁面已經(jīng)改變的值是不會(huì)重置的
緩存策略
使用hashtable+雙向鏈表來存儲(chǔ)RCTBridge實(shí)例,count或cost超出閾值,會(huì)按照LRU策略清理沒有使用的RCTBridge實(shí)例;LRU是近期最少使用的算法,它的核心思想是當(dāng)緩存滿時(shí),會(huì)優(yōu)先淘汰那些近期最少使用的緩存對(duì)象,提升緩存命中;我們先來看下LRU策略運(yùn)作方式:

LRU緩存實(shí)現(xiàn)類似于一個(gè)特殊的棧,把訪問過的元素放置到棧頂(若棧中存在,則更新至棧頂;若棧中不存在則直接入棧),然后如果棧中元素?cái)?shù)量超過限定值,則刪除棧底元素(即最近最少使用的元素)
存儲(chǔ)結(jié)構(gòu)
使用hashtable可以在在時(shí)間復(fù)雜度O(1)內(nèi)完成數(shù)據(jù)訪問;使用雙向鏈表實(shí)現(xiàn),可以在時(shí)間復(fù)雜度O(1)內(nèi)完成刪除和插入的操作;保存方式如下:

key和value
- key: businessURL + commonURL
- value: LinkedNode
按照拆包的模式下,common包和business包會(huì)分別進(jìn)行codepush熱更新檢查,詳見codepush支持多bundle更新重構(gòu),那么安裝完畢以后兩者其中之一的bundle路徑有可能改變,那么原來通過緩存存儲(chǔ)的RCTBridge和現(xiàn)有common+business加載完成的RCTBridge是有差異的,不能單純的只靠bundle名來作為key值,而必須通過businessURL + commonURL成對(duì)的bundle路徑作為key值。
使用LinkedNode作為雙向鏈表的數(shù)據(jù)載體,存儲(chǔ)前后關(guān)系,頭部數(shù)據(jù)prev和尾部數(shù)據(jù)的next值為空;具體結(jié)構(gòu)如下:
@interface LinkedNode : NSObject
@property (nonatomic, copy) NSString* key;
@property (nonatomic, Strong) RCTBridge* value;
@property (nonatomic, Strong) LinkedNode* next;
@property (nonatomic, Strong) LinkedNode* prev;
@end
實(shí)際情況分析
傳統(tǒng)的memryCache在存入新的數(shù)據(jù)時(shí),count或cost超出閾值的情況下使用LRU淘汰策略,如果對(duì)象釋放不需要再額外執(zhí)行invalidate等代碼,一般都是由系統(tǒng)自帶的GC自動(dòng)處理內(nèi)存,就算有別的控制器正在使用也不會(huì)立即釋放,只有當(dāng)對(duì)象處于無引用狀態(tài)下才會(huì)參與釋放流程;然而在我們的拆包項(xiàng)目中,RCTBridge實(shí)例被創(chuàng)建并使用后,內(nèi)部創(chuàng)建了一些計(jì)時(shí)器等模塊造成循環(huán)引用,需要執(zhí)行invalidate才能斷開釋放內(nèi)存,如果某個(gè)控制器正在使用RCTBridge實(shí)例,且剛好出現(xiàn)LRU淘汰策略執(zhí)行了invalidate,那么該控制器就會(huì)出現(xiàn)意想不到的后果,所以必須手動(dòng)對(duì)RCTBridge實(shí)例的使用情況進(jìn)行計(jì)算,確保沒有控制器使用的情況下才會(huì)參與LRU淘汰策略;
既然是手動(dòng)對(duì)RCTBridge實(shí)例的使用情況進(jìn)行計(jì)算,那么執(zhí)行LRU淘汰策略的時(shí)機(jī)改成當(dāng)某個(gè)實(shí)例使用次數(shù)變成0的情況下執(zhí)行更加合理,試想,每次存入新的實(shí)例情況下,其他實(shí)例也處于正在使用狀態(tài)就算執(zhí)行淘汰策略也是浪費(fèi);
實(shí)現(xiàn)流程
(1)每次存儲(chǔ)的時(shí)候?qū)?dāng)前LinkedNode記錄在棧頂,記錄最后一個(gè)LinkedNode;
(2)當(dāng)某個(gè)RCTBridge實(shí)例使用次數(shù)變成0的情況下觸發(fā)檢查緩存使用情況,從最后一個(gè)LinkedNode向前遍歷,一旦找到未使用的RCTBridge實(shí)例將其清理且調(diào)用invalidate;
(3)當(dāng)執(zhí)行LRU淘汰策略以后,需要對(duì)刪除的LinkedNode前后兩個(gè)LinkedNode重新創(chuàng)建鏈接關(guān)系,如果刪除的是最后一個(gè)LinkedNode需要將前一個(gè)LinkedNode作為最后一個(gè)標(biāo)識(shí)。
LRU算法注意要點(diǎn)
- 在存入數(shù)據(jù)的時(shí)候,發(fā)現(xiàn)key已經(jīng)存在,直接獲取到LinkedNode將其記錄在棧頂,放置到棧頂以后要將prev清空
- 如發(fā)現(xiàn)需要置頂?shù)腖inkedNode原先處于棧尾,將LinkedNode的prev數(shù)據(jù)作為新的棧尾
- 雙向鏈表每次重新建立鏈接關(guān)系,需要考慮線程安全問題,通過加鎖的方式解決