1. 程式人生 > >面試常考點之RecyclerView回收和複用機制最全分析

面試常考點之RecyclerView回收和複用機制最全分析

開始

最近在研究 RecyclerView 的回收複用機制,順便記錄一下。我們知道,RecyclerView 在 layout 子 View 時,都通過回收複用機制來管理。網上關於回收複用機制的分析講解的文章也有一大堆了,分析得也都很詳細,什麼四級快取啊,先去 mChangedScrap 取再去哪裡取啊之類的;但其實,我想說的是,RecyclerView 的回收複用機制確實很完善,覆蓋到各種場景中,但並不是每種場景的回收複用時都會將機制的所有流程走一遍的。舉個例子說,在 setLayoutManager、setAdapter、notifyDataSetChanged 或者滑動時等等這些場景都會觸發回收複用機制的工作。但是如果只是 RecyclerView 滑動的場景觸發的回收複用機制工作時,其實並不需要四級快取都參與的。

問題

假設有一個20個item的RecyclerView,每五個佔滿一個螢幕,在從頭滑到尾的過程中,onCreatViewHolder會呼叫多少次?

正題

RecyclerView 的回收複用機制的內部實現都是由 Recycler 內部類實現,下面就都以這樣一種頁面的滑動場景來講解 RecyclerView 的回收複用機制。

相應的版本:

RecyclerView:  recyclerview-v7-25.1.0.jar

LayoutManager:  GridLayoutManager extends LinearLayoutManager (recyclerview-v7-25.1.0.jar)

這個頁面每行可顯示5個卡位,每個卡位的 item 佈局 type 一致。開始分析回收複用機制之前,先提幾個問題:

Q1:如果向下滑動,新一行的5個卡位的顯示會去複用快取的 ViewHolder,第一行的5個卡位會移出螢幕被回收,那麼在這個過程中,是先進行復用再回收?還是先回收再複用?還是邊回收邊複用?也就是說,新一行的5個卡位複用的 ViewHolder 有可能是第一行被回收的5個卡位嗎?
回答問題之前,先看幾張圖片:
先向下再向上滑動

Picture

黑框表示螢幕,RecyclerView 先向下滑動,第三行卡位顯示出來,再向上滑動,第三行移出螢幕,第一行顯示出來。我們分別在 Adapter 的 onCreateViewHolder() 和 onBindViewHolder() 裡打日誌,下面是這個過程的日誌:

紅框1是 RecyclerView 向下滑動操作的日誌,第三行5個卡位的顯示都是重新建立的 ViewHolder ;紅框2是再次向上滑動時的日誌,第一行5個卡位的重新顯示用的 ViewHolder 都是複用的,因為沒有 create viewHolder 的日誌,然後只有後面3個卡位重新繫結資料,呼叫了onBindViewHolder();那麼問題來了:

Q2: 在這個過程中,為什麼當 RecyclerView 再次向上滑動重新顯示第一行的5個卡位時,只有後面3個卡位觸發了 onBindViewHolder() 方法,重新繫結資料呢?明明5個卡位都是複用的。

在上面的操作基礎上,我們繼續往下操作:先向下再向下

在第二個問題操作的基礎上,目前已經建立了15個 ViewHolder,此時顯示的是第1、2行的卡位,那麼繼續向下滑動兩次,這個過程的日誌如下:

紅框1是第二個問題操作的日誌,在這裡截出來只是為了顯示接下去的日誌是在上面的基礎上繼續操作的;

紅框2就是第一次向下滑時的日誌,對比問題2的日誌,這次第三行的5個卡位用的 ViewHolder 也都是複用的,而且也只有後面3個卡位觸發了 onBindViewHolder() 重新繫結資料;

紅框3是第二次向下滑動時的日誌,這次第四行的5個卡位,前3個的卡位用的 ViewHolder 是複用的,後面2個卡位的 ViewHolder 則是重新建立的,而且5個卡位都呼叫了 onBindViewHolder() 重新繫結資料;

Q3:接下去不管是向上滑動還是向下滑動,滑動幾次,都不會再有 onCreateViewHolder() 的日誌了,也就是說 RecyclerView 總共建立了17個 ViewHolder,但有時一行的5個卡位只有3個卡位需要重新繫結資料,有時卻又5個卡位都需要重新繫結資料,這是為什麼呢?

如果明白 RecyclerView 的回收複用機制,那麼這三個問題也就都知道原因了;反過來,如果知道這三個問題的原因,那麼理解 RecyclerView 的回收複用機制也就更簡單了;所以,帶著問題,在特定的場景下去分析原始碼的話,應該會比較容易。

原始碼分析

其實,根據問題2的日誌,我們就可以回答問題1了。在目前顯示1、2行,ViewHolder 的個數為10個的基礎上,第三行的5個新卡位要顯示出來都需要重新建立 ViewHolder,也就是說,在這個向下滑動的過程,是5個新卡位的複用機制先進行工作,然後第1行的5個被移出螢幕的卡位再進行回收機制工作。那麼,就先來看看複用機制的原始碼。

複用機制

getViewForPosition()

//入口在這裡
public View getViewForPosition(int position) {
   return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
   return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
           boolean dryRun, long deadlineNs)
{  
   //複用機制工作原理都在這裡
   //...
}

這個方法是複用機制的入口,也就是 Recycler 開放給外部使用複用機制的api,外部呼叫這個方法就可以返回想要的 View,而至於這個 View 是複用而來的,還是重新建立得來的,就都由 Recycler 內部實現,對外隱藏。

tryGetViewHolderForPositionByDeadline()

所以,Recycler 的複用機制內部實現就在這個方法裡。分析邏輯之前,先看一下 Recycler 的幾個結構體,用來快取 ViewHolder 的。

 public final class Recycler { 
   final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
   ArrayList<ViewHolder> mChangedScrap = null;
   //這個是本篇的重點
   final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

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

   private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
   int mViewCacheMax = DEFAULT_CACHE_SIZE;
   //這個也是本篇的重點
   RecycledViewPool mRecyclerPool;

   private ViewCacheExtension mViewCacheExtension;

   static final int DEFAULT_CACHE_SIZE = 2;
}

mAttachedScrap:用於快取顯示在螢幕上的 item 的 ViewHolder,場景好像是 RecyclerView 在 onLayout 時會先把 children 都移除掉,再重新新增進去,所以這個 List 應該是用在佈局過程中臨時存放 children 的,反正在 RecyclerView 滑動過程中不會在這裡面來找複用的 ViewHolder 就是了

mChangedScrap: 這個沒理解是幹嘛用的,看名字應該跟 ViewHolder 的資料發生變化時有關吧,在 RecyclerView 滑動的過程中,也沒有發現到這裡找複用的 ViewHolder,所以這個可以先暫時放一邊。

mCachedViews:這個就重要得多了,滑動過程中的回收和複用都是先處理的這個 List,這個集合裡存的 ViewHolder 的原本資料資訊都在,所以可以直接新增到 RecyclerView 中顯示,不需要再次重新 onBindViewHolder()。

mUnmodifiableAttachedScrap: 不清楚幹嘛用的,暫時跳過。

mRecyclerPool:這個也很重要,但存在這裡的 ViewHolder 的資料資訊會被重置掉,相當於 ViewHolder 是一個重創新建的一樣,所以需要重新呼叫 onBindViewHolder 來繫結資料。

mViewCacheExtension:這個是留給我們自己擴充套件的,好像也沒怎麼用,就暫時不分析了。

那麼接下去就看看複用的邏輯:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, 
               boolean dryRun, long deadlineNs)
{
   if (position < 0 || position >= mState.getItemCount()) {
       throw new IndexOutOfBoundsException("Invalid item position " + position
               + "(" + position + "). Item count:" + mState.getItemCount());
   }
   //...省略程式碼
}
第一步很簡單,position 如果在 item 的範圍之外的話,那就拋異常吧。繼續往下看:

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
               boolean dryRun, long deadlineNs)
{
   //...省略看過的程式碼
   boolean fromScrapOrHiddenOrCache = false;
   ViewHolder holder = null;
   // 0) If there is a changed scrap, try to find from there
   //上面是Google留的註釋,大意是...(emmm,這裡我也沒理解)
   if (mState.isPreLayout()) {
       holder = getChangedScrapViewForPosition(position);
       fromScrapOrHiddenOrCache = holder != null;
   }
}

如果是在 isPreLayout() 時,那麼就去 mChangedScrap 中找。那麼這個 isPreLayout 表示的是什麼?共5有個賦值的地方。

//只顯示相關程式碼,無關程式碼省略 
protected void onMeasure(int widthSpec, int heightSpec) {
   if (mLayout.mAutoMeasure) {
       //...
   } else {
       // custom onMeasure
       if (mAdapterUpdateDuringMeasure) {
           if (mState.mRunPredictiveAnimations) {
               mState.mInPreLayout = true;
           } else {
               // consume remaining updates to provide a consistent state with the layout pass.
               mAdapterHelper.consumeUpdatesInOnePass();
               mState.mInPreLayout = false;
           }
       }  
   }
   //...
   mState.mInPreLayout = false; // clear
}

private void dispatchLayoutStep1() {
   //...
   mState.mInPreLayout = mState.mRunPredictiveAnimations;
   //...
}

private void dispatchLayoutStep2() {
   //...
   mState.mInPreLayout = mState.mRunPredictiveAnimations;
   mLayout.onLayoutChildren(mRecycler, mState);
   //...
}

emmm,看樣子,在 LayoutManager 的 onLayoutChildren 前就會置為 false,不過我還是不懂這個過程是幹嘛的,滑動過程中好像 mState.mInPreLayou = false,所以並不會來這裡,先暫時跳過,繼續往下。

ViewHolder tryGetViewHolderForPositionByDeadline(int position, 
               boolean dryRun, long deadlineNs)
{
   //...省略看過的程式碼
   // 1) Find by position from scrap/hidden list/cache
   if (holder == null) {
       //這裡是第一次找可複用的ViewHolder了,得跟進去看看
       holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
       //...
   }
}

跟進這個方法看看:

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position,boolean dryRun) { 
   final int scrapCount = mAttachedScrap.size();

   // Try first for an exact, non-invalid match from scrap.
   for (int i = 0; i < scrapCount; i++) {
       //首先去mAttachedScrap中遍歷尋找,匹配條件也很多
       final ViewHolder holder = mAttachedScrap.get(i);
       if (!holder.wasReturnedFromScrap() &amp;&amp; holder.getLayoutPosition() == position
               &amp;&amp; !holder.isInvalid() &amp;&amp; (mState.mInPreLayout || !holder.isRemoved())) {
           holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
           return holder;
       }
   }
   //省略程式碼...
}

首先,去 mAttachedScrap 中尋找 position 一致的 viewHolder,需要匹配一些條件,大致是這個 viewHolder 沒有被移除,是有效的之類的條件,滿足就返回這個 viewHolder。所以,這裡的關鍵就是要理解這個 mAttachedScrap 到底是什麼,存的是哪些 ViewHolder。一次遙控器按鍵的操作,不管有沒有發生滑動,都會導致 RecyclerView 的重新 onLayout,那要 layout 的話,RecyclerView 會先把所有 children 先 remove 掉,然後再重新 add 上去,完成一次 layout 的過程。那麼這暫時性的 remove 掉的 viewHolder 要存放在哪呢,就是放在這個 mAttachedScrap 中了,這就是我的理解了。所以,感覺這個 mAttachedScrap 中存放的 viewHolder 跟回收和複用關係不大。

網上一些分析的文章有說,RecyclerView 在複用時會按順序去 mChangedScrap, mAttachedScrap 等等快取裡找,沒有找到再往下去找,從程式碼上來看是這樣沒錯,但我覺得這樣表述有問題。因為就我們這篇文章基於 RecyclerView 的滑動場景來說,新卡位的複用以及舊卡位的回收機制,其實都不會涉及到 mChangedScrap 和 mAttachedScrap,所以我覺得還是基於某種場景來分析相對應的回收複用機制會比較好。就像 mChangedScrap 我雖然沒理解是幹嘛用的,但我猜測應該是在當資料發生變化時才會涉及到的複用場景,所以當我分析基於滑動場景時的複用時,即使我對這塊不理解,影響也不會很大。繼續向下看:


            
           

相關推薦

面試考點RecyclerView回收機制分析

開始最近在研究 RecyclerView 的回收複用機制,順便記錄一下。我們知道,RecyclerView 在 layout 子 View 時,都通過回收複用機制來管理。網上關於回收複用機制的分析講解的文章也有一大堆了,分析得也都很詳細,什麼四級快取啊,先去 mChangedScrap 取再去哪裡取啊之類的;

面試考點:httphttps的區別與聯系

vps sock 選擇 請求 網站 國家 報文 體系 soc 超文本傳輸協議HTTP協議被用於在Web瀏覽器和網站服務器之間傳遞信息,HTTP協議以明文方式發送內容,不提供任何方式的數據加密,如果攻擊者截取了Web瀏覽器和網站服務器之間的傳輸報文,就可以直接讀懂其中的信息,

【JAVA面試】JAVA考點資料結構與演算法(1)

                            JAVA常考點之資料結構與演算法(1) JAVA常考點之資料結構與演算法 目錄

面試考點:httphttps的區別與聯絡

感謝原作者,本文轉載自http://www.mahaixiang.cn/internet/1233.html 超文字傳輸協議HTTP協議被用於在Web瀏覽器和網站伺服器之間傳遞資訊,HTTP協議以明文方式傳送內容,不提供任何方式的資料加密,如果攻擊者截取了Web瀏覽器和網站伺服器之間的傳輸報文,

計算機網路基礎知識面試考點

1、OSI,TCP/IP,五層協議的體系結構,以及各層協議   OSI分層 (7層):物理層、資料鏈路層、網路層、傳輸層、會話層、表示層、應用層。 TCP/IP分層(4層):網路介面層、 網際層、運輸層、 應用層。 五層協議     (5層):物理

計算機網路部分的面試考點

整理一下計算機網路部分的面試常考點,參考書籍:《計算機網路》第五版 謝希仁的那本,希望對大家有所幫助 OSI,TCP/IP,五層協議的體系結構,以及各層協議 OSI分層 (7層):物理層、資料鏈路層、網路層、傳輸層、會話層、表示層、應用層。 TCP/

基於場景解析RecyclerView回收機制原理

最近在研究 RecyclerView 的回收複用機制,順便記錄一下。我們知道,RecyclerView 在 layout 子 View 時,都通過回收複用機制來管理。網上關於回收複用機制的分析講解的文章也有一大堆了,分析得也都很詳細,什麼四級快取啊,先去 mChangedScrap 取再去哪裡取啊之

python面試考點

HTTP協議: HTTP是超文字傳輸協議的簡稱,他是TCP/IP協議的一個應用層協議,用於定義web瀏覽器與web伺服器之間互動資料的過程。客戶端連上web伺服器後,若想獲得web伺服器中的某個web資源,須遵循一定的通訊格式,HTTP用於定義客戶端與web伺服器之間的通訊格式。 IP

【原創】Selenium學習系列(七)—ConnectDB測試方法

一篇來說一下Webdriver中連線DB合複用測試方法。 兩個完全不搭邊的東西怎麼說明呢,既然不好說那就不多說,通過例子來理解。 需求我們要實現一個這樣的測試情境: 登入系統時,若loginID正確,但密碼錯誤,連續三次密碼輸入錯誤後,系統會lock user。 怎麼實現呢

面試問題整理TCP/IP網路程式設計

本文為本人面試問題中關於TCP/IP和網路程式設計的整理彙總。 (1)常見問題 1.TCP和UDP的區別? TCP面向連線(如打電話要先撥號建立連線);UDP是無連線的,即傳送資料之前不需要建立連線 TCP提供可靠的服務。也就是說,通過

面試考點工廠模式

什麼是工廠模式?工廠模式(Factory Pattern)是 Java 中最常用的設計模式之一。在工廠模式中,我們在建立物件時不會對客戶端暴露建立邏輯,並且是通過使用一個共同的介面來指向新建立的物件。為什麼要使用工廠模式?優缺點?使用場景?使用目的:定義一個建立物件的介面,讓

事務四大隔離級別(面試考點

事務隔離級別:@Transactional(isolation = Isolation.READ_UNCOMMITTED) 讀取未提交資料(會出現髒讀, 不可重複讀) 基本不使用 @Transactional(isolation = Isolation.READ_COMM

併發程式設計程序,多路,multiprocess模組

併發 1. 背景知識 2. 什麼是程序 3. 程序排程 4. 併發與並行 5 同步\非同步\阻塞\非阻塞(重點) 6.multiprocess模組 7.殭屍程序與孤兒程序1.背景知識一作業系統的作用: 1:隱藏醜陋複雜的硬體介面,提供良好的抽象介面 2:管

RecyclerView機制

上一篇文章分析RecyclerView重新整理機制時知道LayoutManager在佈局子View時會向Recycler索要一個ViewHolder。但從Recycler中獲取一個ViewHolder的前提是Recycler中要有ViewHolder。那Recycler中是如何有ViewHolder的呢

Stringstream 緩衝區清空方法 StringStream 不是clear那麼簡單

最近編寫程式時用到 int型別-->string型別的東西,後來發現結果老是有點不對。現在才知道是Stringstream  惹的禍。 用clear的話,記憶體會不斷增長。 在for迴圈中每次呼叫strStream.clear()是希望在每次使用完strStream之後清理s

python實戰IO多路(別名:事件驅動,三種模式:(sellect,poll,epoll),Python的selectors模組)

IO多路複用前需瞭解 通常,我們寫伺服器處理模型的程式時,有以下幾種模型: (1)每收到一個請求,建立一個新的程序,來處理該請求; (2)每收到一個請求,建立一個新的執行緒,來處理該請求; (3)每收到一個請求,放入一個事件列表,讓主程序通過非阻塞I/O方式來處理請求 上面的幾種

安全路 —— 利用埠技術隱藏後門埠

簡介 前面我們介紹到我們可以用程序注入的方法,借用其他應用的埠收發資訊,從而達到穿牆的效果,那麼今天介紹一種新的方法,叫做埠複用技術,他能夠與其他應用繫結同一個埠,但同時進行埠複用的程式會接管之前程式的資訊接受權,所以我們在複用埠後,要對非後門資訊通過1

Java 8 Stream簡介問題

最近工作後開始使用Stream,用起來比較順手,可以說已經“沉浸於Stream無法自拔”,很少再用foreach迴圈了。 其中的Collectors.toMap 和 Collectors.groupingBy等操作好用到爆。 但是糾結於“Stream複用”問題。 這

圖解阻塞io非阻塞io及多路機制

文章目錄IOTCP通訊阻塞IO非阻塞IO IO 即Input Stream與Output Stream TCP通訊 在介紹IO之前我們首先我們先了解一下TCP協議,對於TCP通訊來說,每個TCP的scoket核心裡面都有一個接受與傳送緩衝區。 資料在應用層的s

repo詳解與如何更改manifest快速獲取AOSP程式碼

源起 現在很多企業的網路一般都比較快, 但是有的企業卻會限速, 如果需要從github和google code上面git clone大的倉庫的話, 那麼需要耗費的時間是很客觀的,  例如從github或者google code, 或者其他託管服務站點獲取Android中需要