RecyclerView中ViewHolder重用機制理解(解決圖片錯亂和閃爍問題)
對於使用ViewHolder引起的圖片錯亂問題,相信大部分人都有遇到過,我也一樣,對於解決方法也有所瞭解,但一直都是知其然不知其所以然。
所以,這次直接把ViewHolder的工作原理,通過簡單的demo程式碼來驗證一次,驗證後對於圖片錯亂和閃爍這種問題的成因就很清楚了。
下面先上一副圖
這幅圖就比較清晰的畫出了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可以驗證。
這時候我們往下滾動RecyclerView,再看Log。可可以看到,位置0的資料HODV-21194和位置2的資料IPZ-777所在的ViewHolder被釋放,位置10和位置11的資料分別被載入,這個時候,由於onBindViewHolder()在為TextView設定資料前先列印了TextView裡面的資料,恰恰就是剛才被回收掉的資料,所以可以驗證新繫結的兩個ViewHolder物件就是剛才被回收掉的兩個ViewHolder。
同理,當我們把螢幕再次往上滾動時,在螢幕下面超出顯示範圍的item會被回收,並重用到上面的item中。下圖Log可以看出,位置11和位置9的資料被回收並重用。
查詢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上圖片變來變去,錯亂更加嚴重。
解決方法:
解決方法其實有很多種,這裡列出兩種情況:
- 當item還在載入圖片的過程中,被移出螢幕可視範圍,不需要繼續載入這張圖片了,可以在onRecycled中取消圖片的載入。這樣就不會造成圖片載入完成設定到其他item的ImageView中了。
- 每一個經過螢幕可視區域的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引數。
相關推薦
RecyclerView中ViewHolder重用機制理解(解決圖片錯亂和閃爍問題)
對於使用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
Redhat7中yum安裝以及問題解決辦法(YUM和wget都無法使用的情況下)
簡介 在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編碼的圖片在網頁中的顯示問題的解決
除了信號觸發線程與接收者線程相同的情況能直接調用到slot,其它情況都依賴事件機制(解決上面代碼收不到信號的問題其實很簡單,在線程的run();函數中添加一個事件循環就可以了,即加入一句exec();)
使用 usleep tle 結果 線程 方法 params str signal MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
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,然後歷盡千辛萬苦找到了實現視訊圓角的解決方案,但卻發現又進入了另一個坑,一個非常詭異的異常,如下圖
struts中ModelDriven()介面 Struts2中的ModelDriven機制及其運用、refreshModelBeforeResult屬性解決的問題
Struts2中的ModelDriven機制及其運用、refreshModelBeforeResult屬性解決的問題 1.為什麼需要ModelDriven? 所謂ModelDriven,意思是直接把實體類當成頁面資料的收集物件。比如,有實體類User如下:
《轉》maven中import scope依賴方式解決單繼承問題的理解
在maven多模組專案中,為了保持模組間依賴的統一,常規做法是在parent model中,使用dependencyManagement預定義所有模組需要用到的dependency(依賴) <dependencyManagement>
【iOS】利用cell的重用機制取消圖片非同步下載
在專案中,我們會進行非同步的網路下載圖片把它載入UITableViewCell中上,一般情況下在我們會在cellForRow方法裡面設定cell的圖片資料來源(非同步進行網路下載圖片),也就是說如果一個cell的UIImageview物件開啟了一個下載任務,這個