1. 程式人生 > >RecyclerView中ViewHolder重用機制理解(解決圖片錯亂和閃爍問題)

RecyclerView中ViewHolder重用機制理解(解決圖片錯亂和閃爍問題)

對於使用ViewHolder引起的圖片錯亂問題,相信大部分人都有遇到過,我也一樣,對於解決方法也有所瞭解,但一直都是知其然不知其所以然。

所以,這次直接把ViewHolder的工作原理,通過簡單的demo程式碼來驗證一次,驗證後對於圖片錯亂和閃爍這種問題的成因就很清楚了。

下面先上一副圖

ViewHolder工作原理

這幅圖就比較清晰的畫出了ViewHolder的工作原理。

可以看到,圖中左上角item1上面有一條藍色的線,item7下面也有一條藍色的線,這兩條線就是螢幕的上下邊緣,我們在螢幕中能看到的內容就是item1~item7。

當我們控制螢幕向下滾動時,螢幕上的變化是,item1離開了螢幕,緊接著item8進入了螢幕,這是我們看到的。在item1離開,item8進入的過程中,還有一個我們看不到的過程。當item1離開螢幕時,它會進入Recycler(反覆迴圈器)構件,然後被放到了item8的位置,成為了我們看到的item8。

通過程式碼來驗證這個變化過程

下面是MainActivity的程式碼 
初始化了12條資料( 這真的是正經資料 ╮( ̄▽ ̄”)╭ ) 
初始化Adapter並設定到RecyclerView

public class MainActivity extends AppCompatActivity {
    private RecyclerView recyclerView;
    private List<String> mData;
    private MyRecyclerAdapter recycleAdapter;
    @Override
    protected
void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = (RecyclerView) findViewById(R.id.id_recyclerView); initData(); recycleAdapter = new MyRecyclerAdapter(MainActivity.this, mData); // ...
recyclerView.setAdapter(recycleAdapter); // ... } private void initData() { mData = new ArrayList<>(); mData.add("HODV-21194"); //0 mData.add("TEK-080"); //1 mData.add("IPZ-777"); //2 mData.add("MIMK-045"); //3 mData.add("HODV-21193"); //4 mData.add("MIDE-339"); //5 mData.add("IPZ-780"); //6 mData.add("VEC-205"); //7 mData.add("VEMA-113"); //8 mData.add("IPZ-776"); //9 mData.add("MIAD-923"); //10 mData.add("ARM-513"); //11 } }

下面是Adapter部分,為了更方便驗證,程式碼非常簡單,ViewHolder裡面只有一個TextView。

public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.MyViewHolder> {
    private static final String TAG = "MyRecyclerAdapter";
    private List<String> mData;
    private Context mContext;
    private LayoutInflater inflater;
    public MyRecyclerAdapter(Context context, List<String> data) {
        this.mContext = context;
        this.mData = data;
        inflater = LayoutInflater.from(mContext);
    }
    @Override
    public int getItemCount() {
        return mData.size();
    }
    @Override
    public void onViewRecycled(MyViewHolder holder) {
        super.onViewRecycled(holder);
        Log.d(TAG, "onViewRecycled: "+holder.tv.getText().toString()+", position: "+holder.getAdapterPosition());
    }
    //填充onCreateViewHolder方法返回的holder中的控制元件
    @Override
    public void onBindViewHolder(final MyViewHolder holder, final int position) {
        Log.d(TAG, "onBindViewHolder: 驗證是否重用了");
        Log.d(TAG, "onBindViewHolder: 重用了"+holder.tv.getTag());
        Log.d(TAG, "onBindViewHolder: 放到了"+mData.get(position));
        holder.tv.setText(mData.get(position));
        holder.tv.setTag(mData.get(position));
    }
    //重寫onCreateViewHolder方法,返回一個自定義的ViewHolder
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Log.d(TAG, "onCreateViewHolder");
        View view = inflater.inflate(R.layout.item_layout, parent, false);
        return new MyViewHolder(view);
    }
    static class MyViewHolder extends RecyclerView.ViewHolder {
        TextView tv;
        public MyViewHolder(View view) {
            super(view);
            tv = (TextView) view.findViewById(R.id.id_num);
        }
    }
}

簡單瞭解上面程式碼的執行邏輯,並關注onCreateViewHolder()、onBindViewHolder()、onViewRecycled()三個方法列印的Log日誌,下面通過列印的Log分析驗證ViewHolder的建立、釋放與複用。

當第一次開啟應用載入RecyclerView時,可以觀察到在螢幕中我們看到的每一個item都經過onCreateViewHolder()建立了一個ViewHolder物件,textView中的tag都為null。下圖中紅色框框中的Log可以驗證。

ViewHolder工作原理

這時候我們往下滾動RecyclerView,再看Log。可可以看到,位置0的資料HODV-21194和位置2的資料IPZ-777所在的ViewHolder被釋放,位置10和位置11的資料分別被載入,這個時候,由於onBindViewHolder()在為TextView設定資料前先列印了TextView裡面的資料,恰恰就是剛才被回收掉的資料,所以可以驗證新繫結的兩個ViewHolder物件就是剛才被回收掉的兩個ViewHolder。

ViewHolder工作原理2

同理,當我們把螢幕再次往上滾動時,在螢幕下面超出顯示範圍的item會被回收,並重用到上面的item中。下圖Log可以看出,位置11和位置9的資料被回收並重用。

ViewHolder工作原理3

查詢ViewHolder出現圖片錯亂的原因

通過上面的內容解釋,瞭解了ViewHolder的重用機制,接下來看一段會出現圖片錯亂的程式碼示例。

public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.MyViewHolder> {
    private static final String TAG = "MyRecyclerAdapter";
    private List<String> mData;
    private Context mContext;
    private LayoutInflater inflater;
    public MyRecyclerAdapter(Context context, List<String> data) {
        this.mContext = context;
        this.mData = data;
        inflater = LayoutInflater.from(mContext);
    }
    @Override
    public int getItemCount() {
        return mData.size();
    }
    @Override
    public void onViewRecycled(MyViewHolder holder) {
        super.onViewRecycled(holder);
        Log.d(TAG, "onViewRecycled: "+holder.imageView.getTag().toString()+", position: "+holder.getAdapterPosition());
    }
    @Override
    public void onBindViewHolder(final MyViewHolder holder, final int position) {
        Log.d(TAG, "onBindViewHolder: 驗證是否重用了");
        Log.d(TAG, "onBindViewHolder: 重用了"+holder.imageView.getTag());
        Log.d(TAG, "onBindViewHolder: 放到了"+mData.get(position));
        holder.imageView.setTag(mData.get(position));
        new AsyncTask<Void, Void, Bitmap>() {
            @Override
            protected Bitmap doInBackground(Void... params) {
                try {
                    URL url = new URL(mData.get(position));
                    Bitmap bitmap = BitmapFactory.decodeStream(url.openStream());
                    return bitmap;
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
            @Override
            protected void onPostExecute(Bitmap bitmap) {
                super.onPostExecute(bitmap);
                holder.imageView.setImageBitmap(bitmap);
            }
        }.execute();
    }
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Log.d(TAG, "onCreateViewHolder");
        View view = inflater.inflate(R.layout.item_layout, parent, false);
        return new MyViewHolder(view);
    }
    static class MyViewHolder extends RecyclerView.ViewHolder {
        ImageView imageView;
        public MyViewHolder(View view) {
            super(view);
            imageView = (ImageView) view.findViewById(R.id.id_img);
        }
    }
}

這段程式碼相對於上一段Adapter的程式碼改動也比較少,只是把TextView改成了ImageView,並在onBindViewHolder()時非同步載入一張網路圖片,當載入完畢把圖片放置到ImageView中顯示。

在不瞭解ViewHolder重用機制之前,這段程式碼看似沒有什麼問題,但事實上這段程式碼由於ViewHolder重用機制的存在,並不能如期執行。

下面使用這段程式碼來分析一下場景。

場景A:

1.第一次執行,RecyclerView載入,不做任何觸控操作 
2.Adapter經過onCreateViewHolder()建立了上面我們能看到的8個ViewHolder物件,並且在onBind時啟動了8條執行緒載入圖片 
3.8張圖片全部載入完畢,並且顯示到對應的ImageView上 
4.控制螢幕向下滾動,第1、第2個item離開螢幕可視區域,第9、第10個item進入螢幕可視區域 
5.第1、第2個item被回收,重用到第9、第10個item。第9、第10個item顯示的圖片是第1和第2個item的圖片!!! 
6.開啟了兩條執行緒,載入第9、第10張圖片。等待幾秒,第9、第10個item顯示的圖片突然變成了正確的圖片!

以上過程是場景A,經過拆分細化,非常容易看出問題所在。如果當前網路速度很快,第6個步驟的載入速度在1秒甚至0.5秒內,就會造成人眼看到的圖片閃爍問題出現,第9、第10個item的圖片閃了一下變成了正確的圖片。

場景B:

1.第一次執行,RecyclerView載入 
2.Adapter經過onCreateViewHolder()建立了上面我們能看到的8個ViewHolder物件,並且在onBind時啟動了8條執行緒載入圖片 
3.7張圖片載入完畢,還有1張未載入完(已知圖片一載入速度異常慢) 
4.控制螢幕向下滾動,第1、第2個item離開螢幕可視區域,第9、第10個item進入螢幕可視區域 
5.第1、第2個item被回收,重用到第9、第10個item。閃爍問題不再重複說,第9、第10張圖片載入完畢(看上去一切正常) 
6.等待幾秒,第一張圖片終於載入完成,第9個item突然從正確的圖片九變成不正確的圖片一 !!!

以上過程是場景B,問題出現在載入第一張圖片的執行緒T,持有了item1的ImageView物件引用,而這張圖片載入速度非常慢,直到item1已經被重用到item9後,過了一段時間,執行緒T才把圖片一加載出來,並設定到item1的ImageView上,然而執行緒T並不知道item1已經不存在且變成了item9,於是,圖片發生錯亂了。

場景C:

1.第一次執行,RecyclerView載入 
2.Adapter經過onCreateViewHolder()建立了上面我們能看到的8個ViewHolder物件,並且在onBind時啟動了8條執行緒載入圖片 
3.忽略圖片載入情況,直接向下滾動,再向上滾動,再向下滾動,來回操作 
4.由於離開了螢幕的item是隨機被回收並重用的,所以向下滾動時我們假設item1、item3被回收重用到item9、item10,item2、item4被回收重用到item11、item12 
5.向上滾動時,item9、item12被回收重用到item1、item2,item10、item11被回收重用到item3、item4 
6.多次上下滾動後,停下,最後發現某一個item的圖片在不停變化,最後還不一定是正確的圖片

以上過程是場景C,問題出現在ViewHolder的回收重用順序是隨機的,回收時會從離開螢幕範圍的item中隨機回收,並分配給新的item,來回運算元次,就會造成有多條載入不同圖片的執行緒,持有同一個item的ImageView物件,造成最後在同一個item上圖片變來變去,錯亂更加嚴重。

解決方法:

解決方法其實有很多種,這裡列出兩種情況:

  1. 當item還在載入圖片的過程中,被移出螢幕可視範圍,不需要繼續載入這張圖片了,可以在onRecycled中取消圖片的載入。這樣就不會造成圖片載入完成設定到其他item的ImageView中了。
  2. 每一個經過螢幕可視區域的item,載入的圖片都要放進快取中,即使item離開了可視區域,也要載入完畢並放入快取中,方便下次瀏覽時能快速載入。每次onBind時對ImageView設定Tag標記,如果Tag標記已經被更改,舊執行緒載入好的圖片不再設定到ImageView中。

當然以上兩種情況都別忘了先設定圖片佔位符,防止回收item的圖片直接顯示到新item中。

解決方式1 demo程式碼:

public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.MyViewHolder> {
    private static final String TAG = "MyRecyclerAdapter";
    private List<String> mData;
    private Context mContext;
    private LayoutInflater inflater;
    public MyRecyclerAdapter(Context context, List<String> data) {
        this.mContext = context;
        this.mData = data;
        inflater = LayoutInflater.from(mContext);
    }
    @Override
    public int getItemCount() {
        return mData.size();
    }
    @Override
    public void onViewRecycled(MyViewHolder holder) {
        super.onViewRecycled(holder);
        AsyncTask asyncTask = (AsyncTask) holder.imageView.getTag();
        asyncTask.cancel(true);
    }
    @Override
    public void onBindViewHolder(final MyViewHolder holder, final int position) {
        //先設定圖片佔位符
        holder.imageView.setImageDrawable(mContext.getDrawable(R.mipmap.ic_launcher));
        AsyncTask asyncTask = new AsyncTask<Void, Void, Bitmap>() {
            @Override
            protected Bitmap doInBackground(Void... params) {
                try {
                    URL url = new URL(mData.get(position));
                    Bitmap bitmap = BitmapFactory.decodeStream(url.openStream());
                    return bitmap;
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
            @Override
            protected void onPostExecute(Bitmap bitmap) {
                super.onPostExecute(bitmap);
                holder.imageView.setImageBitmap(bitmap);
            }
        };
        holder.imageView.setTag(1,asyncTask);
        asyncTask.execute();
    }
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = inflater.inflate(R.layout.item_layout, parent, false);
        return new MyViewHolder(view);
    }
    static class MyViewHolder extends RecyclerView.ViewHolder {
        ImageView imageView;
        public MyViewHolder(View view) {
            super(view);
            imageView = (ImageView) view.findViewById(R.id.id_img);
        }
    }
}

解決方式2 demo程式碼:

public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.MyViewHolder> {
    private static final String TAG = "MyRecyclerAdapter";
    private List<String> mData;
    private Context mContext;
    private LayoutInflater inflater;
    public MyRecyclerAdapter(Context context, List<String> data) {
        this.mContext = context;
        this.mData = data;
        inflater = LayoutInflater.from(mContext);
    }
    @Override
    public int getItemCount() {
        return mData.size();
    }
    @Override
    public void onViewRecycled(MyViewHolder holder) {
        super.onViewRecycled(holder);
    }
    @Override
    public void onBindViewHolder(final MyViewHolder holder, final int position) {
        //先設定圖片佔位符
        holder.imageView.setImageDrawable(mContext.getDrawable(R.mipmap.ic_launcher));
        final String url = mData.get(position);
        //為imageView設定Tag,內容是該imageView等待載入的圖片url
        holder.imageView.setTag(url);
        AsyncTask asyncTask = new AsyncTask<Void, Void, Bitmap>() {
            @Override
            protected Bitmap doInBackground(Void... params) {
                try {
                    URL url = new URL(mData.get(position));
                    Bitmap bitmap = BitmapFactory.decodeStream(url.openStream());
                    return bitmap;
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
            @Override
            protected void onPostExecute(Bitmap bitmap) {
                super.onPostExecute(bitmap);
                //載入完畢後判斷該imageView等待的圖片url是不是載入完畢的這張
                //如果是則為imageView設定圖片,否則說明imageView已經被重用到其他item
                if(url.equals(holder.imageView.getTag())) {
                    holder.imageView.setImageBitmap(bitmap);
                }
            }
        }.execute();
    }
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = inflater.inflate(R.layout.item_layout, parent, false);
        return new MyViewHolder(view);
    }
    static class MyViewHolder extends RecyclerView.ViewHolder {
        ImageView imageView;
        public MyViewHolder(View view) {
            super(view);
            imageView = (ImageView) view.findViewById(R.id.id_img);
        }
    }
}

上面的解決方式,是最簡單的使用非同步執行緒載入圖片,對於載入圖片有很多第三方庫可以使用,如Picasso、Fresco、Glide等,我們也可以使用這些第三方庫來載入圖片,但使用第三方庫載入的本質還是非同步載入,所以如果處理不當也會出現圖片閃爍等問題,大家可以使用上面的場景ABC等細化分解的步驟來分析錯誤,相信很容易就能找到問題。

注意記憶體洩漏的風險

對於上面的Demo程式碼,其實是存在記憶體洩漏風險的,如果需要使用建議把AsyncTask寫成靜態內部類,以及Adapter初始化時使用ApplicationContext作為引數傳入,不要使用Activity作為Context引數。

相關推薦

RecyclerViewViewHolder重用機制理解(解決圖片錯亂閃爍問題)

對於使用ViewHolder引起的圖片錯亂問題,相信大部分人都有遇到過,我也一樣,對於解決方法也有所瞭解,但一直都是知其然不知其所以然。 所以,這次直接把ViewHolder的工作原理,通過簡單的demo程式碼來驗證一次,驗證後對於圖片錯亂和閃爍這種問題的成因就很清楚了。 下面先上一副圖 這幅圖

Android在RecyclerView巢狀ScrollView,解決兩者間的滑動衝突

在RecyclerView中的item中巢狀一些佈局如TextView,在這種情況下如TextView的字數很多超過所設定的佈局大小。 這樣就需要在item中加一個ScrollView可以用於使用者的滑動。 1.RecyclerView的item佈局如: <?xml

Android效能優化之Listview(ViewHolder重用機制

好久沒發部落格了,因為發部落格太耗時間了,而且參考的比較多,也不想一直帶著轉載兩個字。都一直放在筆記裡。不過只能自己看不能和大家分享了,看到這篇文章令我恍然大悟,很有幫助,所以還是決定寫下來。 相信大家在很多時候都會用到ListView這個控制元件,因為確實是用的很多

iOS開發之Cell重用機制理解

一、UITableView的一些瞭解 代理方法中的 dequeueResableCellWithIdentifier方法,對table view的資料進行繫結,即填充cell,自動呼叫n次 UITab

Redhat7yum安裝以及問題解決辦法(YUMwget都無法使用的情況下)

簡介              在linux系統中,由於Redhat自帶的yum需註冊才可以使用,因此,我們通過安裝centos7.0中的yum代替。        如果直接使用redhat自帶的yum(比如輸入:yum repolist),可能會出現以下提示:This sy

圖片載入庫Glide——解決圖片錯亂+無法設定tag

今天在寫一個圖片載入類ImageLoader,在使用的時候想用Glide替代我寫的ImageLoader,然後問題就出來了!!! 第一個問題:在使用自己寫的ImageLoader的時候,為了防止item複用導致的圖片錯亂,設定了Tag 如下程式碼

Android關於gradled的理解以及如何簽名打包

目錄: 1、gradle的概念 2、gradle配置jar包,和libs資料夾匯入jar包的區別 3、簽名打包:     (1)Studio     (2)命令列     (3)gradle wrapper的原理 4、BuildConfig的使用 5、

ListView複用導致圖片錯亂閃爍問題

關於ViewHolder複用,我就不說明了,關鍵是複用導致出現的問題 網路的好壞,我們請求的圖片並不是很穩定,那麼我們假設一屏可以顯示6條資料,利用複用,我們的第7條資料view就是我們的第1條資料view,隨之手勢不斷的滑動,複用的問題,就出現,特別是上

RecyclerView解決條目錯亂以及圖片閃越+三級快取機制

RecyclerView導致條目錯亂的原因:viewHolder的複用,一個複用的ViewHolder他裡邊的View有些屬性已經被修改了,所以新的item在使用服用的viewHolder時,那些被修改的viewHolder裡邊的屬性還依然存在,所以會導致新的item也應用

解決RecyclerView使用UIL載入網路圖片,在重新整理時出現閃爍問題

      對於開源框架universal-image-loader大多數開發者都不會陌生,的確這是一款很不錯的圖片類框架,值得推薦。github地址是:https://github.com/nostra13/Android,在我之前的部落格中有專門推薦GitHub上幾款比

Android對Handle機制理解

trac 意義 還要 break create findview curl net protected 一、重要參考資料 【參考資料】 眼下來看,以下的幾個網址中的內容質量比較不錯。基本不須要再讀別的網址了。 1、android消息機制一

BASE64編碼的圖片在網頁的顯示問題的解決

base64 問題 圖片 spi www. html nbsp base64編碼 href BASE64編碼的圖片在網頁中的顯示問題的解決 關於圖片的Base64編碼,你了解嗎? BASE64編碼的圖片在網頁中的顯示問題的解決

Redis 哨兵sentinel 機制、從宕機及恢復、主庫宕機及恢復解決方案

目錄 什麼是哨兵 原理 環境 設定哨兵 從宕機及恢復 主宕機及恢復 配置多個哨兵 1、什麼是哨兵 哨兵是對Redis的系統的執行情況的監控,它是一個獨立程序,功能有二個: 監控主資料庫和從資料庫是否執行正常; 主資料出現故障後

Hadoop的JVM重用機制小檔案解決

Hadoop的JVM重用機制和小檔案解決 一、hadoop2.0 uber功能   1) uber的原理:Yarn的預設配置會禁用uber元件,即不允許JVM重用。我們先看看在這種情況下,Yarn是如何執行一個MapReduce job的。首先,Resource Manager裡的App

解決BootStrap輪播圖片圖片大小父div不一致問題

問題出現 其實這個問題相當簡單,自己鼓搗好久,才發現還是自己基本功不紮實,當圖片的大小出現在原生的bootstrap類屬性限定中,圖片會按照自己的大小進行佈局,這樣就會出現圖片小於父div的情況,如下圖所示: 問題解決 找出圖片所屬類,更改類的屬性為blo

GLSurfaceView在recyclerview做itemview豎向滑動時出現遮蓋其他控制元件滑出螢幕的詭異異常解決方案

這幾天遇到了一個需求,recyclerview中的itemview都是圓角矩形的視訊itemview,然後歷盡千辛萬苦找到了實現視訊圓角的解決方案,但卻發現又進入了另一個坑,一個非常詭異的異常,如下圖

strutsModelDriven()介面 Struts2的ModelDriven機制及其運用、refreshModelBeforeResult屬性解決的問題

Struts2中的ModelDriven機制及其運用、refreshModelBeforeResult屬性解決的問題   1.為什麼需要ModelDriven? 所謂ModelDriven,意思是直接把實體類當成頁面資料的收集物件。比如,有實體類User如下:

《轉》mavenimport scope依賴方式解決單繼承問題的理解

在maven多模組專案中,為了保持模組間依賴的統一,常規做法是在parent model中,使用dependencyManagement預定義所有模組需要用到的dependency(依賴) <dependencyManagement>

【iOS】利用cell的重用機制取消圖片非同步下載

在專案中,我們會進行非同步的網路下載圖片把它載入UITableViewCell中上,一般情況下在我們會在cellForRow方法裡面設定cell的圖片資料來源(非同步進行網路下載圖片),也就是說如果一個cell的UIImageview物件開啟了一個下載任務,這個