RecyclerView 簡(jiǎn)析之緩存機(jī)制及優(yōu)化

RecyclerView 是用于大量數(shù)據(jù)展示的控件,相對(duì)于傳統(tǒng)的 ListView 欠雌,更加強(qiáng)大和靈活。

緩存機(jī)制

RecyclerView 與 ListView 的緩存機(jī)制原理大致相似疙筹, 滑動(dòng)的時(shí)候富俄,離屏的 ItemView 被回收至緩存禁炒,入屏的 ItemView 則會(huì)優(yōu)先從緩存中獲取,只是 ListView 與 RecyclerView 的實(shí)現(xiàn)細(xì)節(jié)有差異霍比。

ListView 緩存機(jī)制

ListView 主要是二級(jí)緩存幕袱,緩存的對(duì)象是 View,ListView 是繼承于 AbsListView 的悠瞬,而 AbsListView 里面有個(gè) mRecycler,用于存儲(chǔ)不使用的 view浅妆,其將被下次 layout 的時(shí)候重新使用望迎,以避免創(chuàng)建新的實(shí)例擂煞。

    /**
     * The data set used to store unused views that should be reused during the next layout
     * to avoid creating new ones
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769398)
    final RecycleBin mRecycler = new RecycleBin();

RecycleBin 是 AbsListView 的內(nèi)部類,其作用是通過(guò)兩級(jí)緩存來(lái)緩存 view趴乡。(RecycleBin 在 layout 的過(guò)程中便于 view 重用,RecycleBin 有兩級(jí)緩存:mActiveViews 和 mScrapViews)晾捏。

  • mActiveViews
    第一級(jí)緩存,這些 View 是布局過(guò)程開始時(shí)屏幕上的 view惦辛,layout 開始時(shí)這個(gè)數(shù)組被填充,layout 結(jié)束胖齐,mActiveViews 中的 View 移動(dòng)到 mScrapView,意義在于快速重用屏幕上可見的列表項(xiàng) ItemView呀伙,而不需要重新 createView 和 bindView补履。
  • mScrapView
    第二級(jí)緩存箫锤,mScrapView 是多個(gè) List 組成的數(shù)據(jù),數(shù)組的長(zhǎng)度為 viewTypeCount雨女,每個(gè) List 緩存不同類型 Item 布局的 View,其意義在于緩存離開屏幕的 ItemView氛堕,目的是讓即將進(jìn)入屏幕的 itemView 重用,當(dāng) mAdapter 被更換時(shí)讼稚,mScrapViews 則被清空浪耘。
RecyclerView 緩存機(jī)制

同樣地,RecyclerView 也有一個(gè)類專門來(lái)管理緩存塑崖,不過(guò)與 ListView 不同的是七冲,RecylerView 緩存的是 ViewHolder,而且實(shí)現(xiàn)的是四級(jí)緩存规婆,如下:

public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
private ArrayList<ViewHolder> mChangedScrap = null;

final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

private final List<ViewHolder>
        mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

private RecycledViewPool mRecyclerPool;

private ViewCacheExtension mViewCacheExtension;
  • mAttachedScrap
    第一級(jí)緩存澜躺,相當(dāng)于 ListView 的 mActiveView,快速重用屏幕上可見的 ViewHolder抒蚜。
  • mCacheViews
    第二級(jí)緩存掘鄙,如果仍依賴于 RecyclerView(比如已經(jīng)滑出可視范圍,但還沒有被移除掉)嗡髓,但已經(jīng)被標(biāo)記移除的 ItemView 集合被添加到 mAttachedScrap 中操漠。然后如果 mAttachedScrap 中不再依賴時(shí)會(huì)被加入到 mCachedViews 中,默認(rèn)緩存 2 個(gè) ItemView饿这,RecycleView 從這里獲取的緩存時(shí)浊伙,如果數(shù)據(jù)源不變的情況下,無(wú)需重新 bindView长捧。
  • mViewCacheExtension
    第三級(jí)緩存嚣鄙,其是一個(gè)抽象靜態(tài)類,用于充當(dāng)附加的緩存池串结,當(dāng) RecyclerView 從 mCacheViews 找不到需要的 View 時(shí)哑子,將會(huì)從 ViewCacheExtension 中尋找。不過(guò)這個(gè)緩存是由開發(fā)者維護(hù)的肌割,如果沒有設(shè)置它卧蜓,則不會(huì)啟用。通常我們也不會(huì)設(shè)置它把敞,除非有特殊需求弥奸,比如要在調(diào)用系統(tǒng)的緩存池之前,返回一個(gè)特定的視圖先巴,才會(huì)用到它其爵。
  • RecycledViewPool
    第四級(jí)緩存冒冬,最強(qiáng)大的緩存器伸蚯,代碼如下:
public static class RecycledViewPool {

 // 根據(jù) viewType 保存的被廢棄的 ViewHolder 集合,以便下次使用
 private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<ArrayList<ViewHolder>>();
  /**
   * 從緩存池移除并返回一個(gè) ViewHolder
   */
  public ViewHolder getRecycledView(int viewType) {
    final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
    if (scrapHeap != null && !scrapHeap.isEmpty()) {
      final int index = scrapHeap.size() - 1;
      final ViewHolder scrap = scrapHeap.get(index);
      scrapHeap.remove(index);
      return scrap;
    }
      return null;
    }

  public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    final ArrayList scrapHeap = getScrapHeapForType(viewType);
    if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
      return;
    }
    scrap.resetInternal();
    scrapHeap.add(scrap);
  }

  /**
   * 根據(jù) viewType 獲取對(duì)應(yīng)緩存池
   */
  private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
    ArrayList<ViewHolder> scrap = mScrap.get(viewType);
      if (scrap == null) {
        scrap = new ArrayList<>();
        mScrap.put(viewType, scrap);
          if (mMaxScrap.indexOfKey(viewType) < 0) {
            mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
          }
      }
    return scrap;
  }
}

顧名思義简烤,它是一個(gè)緩存池剂邮,實(shí)現(xiàn)上,是通過(guò)一個(gè)默認(rèn)為 5 大小的 ArrayList 實(shí)現(xiàn)的横侦。這一點(diǎn)挥萌,同 ListView 的 RecyclerBin 這個(gè)類一樣绰姻。每一個(gè) ArrayList 又都是放在一個(gè) Map 里面的,SparseArray 用兩個(gè)數(shù)組用來(lái)替代 Map引瀑。
把所有的 ArrayList 放在一個(gè) Map 里面狂芋,這也是 RecyclerView 最大的亮點(diǎn),這樣根據(jù) itemType 來(lái)取不同的緩存 Holder憨栽,每一個(gè) Holder 都有對(duì)應(yīng)的緩存帜矾,而只需要為這些不同 RecyclerView 設(shè)置同一個(gè) Pool 就可以了。
這個(gè)可以在 Pool 的 setRecycledViewPool() 方法可以看到注釋:

/**
 * Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views.
 * This can be useful if you have multiple RecyclerViews with adapters that use the same
 * view types, for example if you have several data sets with the same kinds of item views
 * displayed by a {@link android.support.v4.view.ViewPager ViewPager}.
 *
 * @param pool Pool to set. If this parameter is null a new pool will be created and used.
 */
public void setRecycledViewPool(RecycledViewPool pool) {
    mRecycler.setRecycledViewPool(pool);
}

RecyclerView 優(yōu)化

  • 數(shù)據(jù)處理和視頻加載分離
    耗時(shí)的數(shù)據(jù)處理邏輯應(yīng)該放在異步處理屡萤,這樣 Adapter 在 notify 改變數(shù)據(jù)時(shí)死陆,ViewHolder 可以操作數(shù)據(jù)于視圖的綁定邏輯措译。比如:
mTextView.setText(Html.fromHtml(data).toString());

這里的 Html.fromHtml(data) 方法可能就是比較耗時(shí)的饰序,存在多個(gè) TextView 的話耗時(shí)會(huì)更為嚴(yán)重,這樣便會(huì)引發(fā)掉幀掠械、卡頓注祖,故此時(shí)應(yīng)該在子線程處理。

  • 數(shù)據(jù)優(yōu)化
    分頁(yè)拉取數(shù)據(jù)時(shí)肚菠,對(duì)拉取下來(lái)的遠(yuǎn)端數(shù)據(jù)進(jìn)行緩存蚊逢,提升二次加載速度箫章;對(duì)于新增或者刪除數(shù)據(jù)通過(guò) DiffUtil 來(lái)進(jìn)行局部刷新數(shù)據(jù),而不是一味地全局刷新數(shù)據(jù)终抽。

  • 減少布局層級(jí)過(guò)渡繪制
    可以通過(guò)自定義 View 或者更合理地設(shè)置布局來(lái)減少層級(jí)昼伴,移除不必要的背景減少過(guò)度繪制。

  • 減少 xml 文件 inflate 時(shí)間
    這里的 xml 文件不僅包括 layout 的 xml价涝,還包括 drawable 的 xml持舆,xml 文件 inflate 出 ItemView 時(shí)通過(guò)耗時(shí)的 IO 操作,尤其當(dāng) Item 的復(fù)用幾率很低的情況下泞遗,對(duì)著 Type 的增多席覆,這種 inflate 帶來(lái)的損耗時(shí)相當(dāng)大的,此時(shí)我們可以用代碼去生成布局聊倔。

  • 減少 View 對(duì)象的創(chuàng)建
    一個(gè)稍微復(fù)雜的 Item 會(huì)包含大量的 View耙蔑,而大量的 View 的創(chuàng)建也會(huì)消耗大量時(shí)間,所以要盡可能簡(jiǎn)化 ItemView甸陌;設(shè)計(jì) ItemType 時(shí)盐股,對(duì)多 ViewType 能夠共用的部分盡量設(shè)計(jì)成自定義 View,減少 View 的構(gòu)造和嵌套牲尺。

  • 使用 RecyclerView 的 prefetch 功能

  • 如果 Item 高度是固定的話幌蚊,可以使用 RecylerView.setHasFixedSize(true),來(lái)避免 requestLayout 浪費(fèi)資源蜒简。

  • 滑動(dòng)過(guò)程沖停止數(shù)據(jù)加載或者圖片加載工作沫换。

  • 如果不需要?jiǎng)赢嫞涯J(rèn)動(dòng)畫關(guān)閉來(lái)提升效率讯赏,動(dòng)畫在 Android 系統(tǒng)中是一個(gè)很大的開銷漱挎。

  • 通過(guò) RecyclerView.setItemViewCacheSize(size);來(lái)加大 RecyclerView 的緩存私爷,用空間換時(shí)間來(lái)提高滾動(dòng)的流暢性膊夹。

  • 如果多個(gè) RecyclerView 的 Adapter 是一樣的,比如嵌套的 RecyclerView 存在一樣的 Adapter工秩,可以通過(guò)設(shè)置 RecyclerView.setRecycledViewPool() 方法來(lái)共用一個(gè) RecyledViewPool进统。

  • 通過(guò) getExtraLayoutSpace() 方法來(lái)增加 RecyclerView 預(yù)留的額外空間(顯示范圍之外,應(yīng)額外緩存空間)眉菱,如下:

new LinearLayoutManager(this) {
    @Override
    protected int getExtraLayoutSpace(RecyclerView.State state) {
        return size;
    }
};
  • 在 onBindView 的時(shí)候只做數(shù)據(jù)綁定數(shù)據(jù)工作掉分,不要?jiǎng)?chuàng)建對(duì)象。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末尔崔,一起剝皮案震驚了整個(gè)濱河市季春,隨后出現(xiàn)的幾起案子载弄,更是在濱河造成了極大的恐慌,老刑警劉巖宇攻,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逞刷,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡仑最,警方通過(guò)查閱死者的電腦和手機(jī)帆喇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門坯钦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人吟温,你說(shuō)我怎么就攤上這事突颊。” “怎么了呈昔?”我有些...
    開封第一講書人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵堤尾,是天一觀的道長(zhǎng)迁客。 經(jīng)常有香客問(wèn)我,道長(zhǎng)掷漱,這世上最難降的妖魔是什么侨嘀? 我笑而不...
    開封第一講書人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任优质,我火速辦了婚禮锋叨,結(jié)果婚禮上形用,老公的妹妹穿的比我還像新娘险掀。我一直安慰自己湾宙,他們只是感情好冈绊,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開白布死宣。 她就那樣靜靜地躺著十电,像睡著了一般知押。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上台盯,一...
    開封第一講書人閱讀 51,604評(píng)論 1 305
  • 那天良价,我揣著相機(jī)與錄音,去河邊找鬼明垢。 笑死市咽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的溯革。 我是一名探鬼主播谷醉,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼俱尼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼遇八!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蔑歌,我...
    開封第一講書人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤揽碘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后劫灶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡供汛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年怔昨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宿稀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趁舀。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡矮烹,死狀恐怖罩锐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情仁期,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站萍聊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏寿桨。R本人自食惡果不足惜此衅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望亭螟。 院中可真熱鬧挡鞍,春花似錦、人聲如沸预烙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)扁掸。三九已至翘县,卻和暖如春最域,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锈麸。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工镀脂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人忘伞。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像翘魄,于是被迫代替她去往敵國(guó)和親训措。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355