RecyclerView中ViewHolder重用机制理解(解决图片错乱和闪烁问题)

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上图片变来变去,错乱更加严重。


解决方法:

解决方法其实有很多种,这里列出两种情况:

  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(1);
        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参数。

对于内存泄漏的相关内容,在另一篇文章有详细的解析,有兴趣可以点链接了解。

Android内存泄漏查找和解决

时间: 2024-08-28 19:38:08

RecyclerView中ViewHolder重用机制理解(解决图片错乱和闪烁问题)的相关文章

关于RecyclerView中Viewholder和View的缓存机制的探究

这个题目放在草稿箱里面许久了,一直没有动力提笔.趁现在公司人还没有来齐,工作量还不是很大,就挤出来时间来把它完善了. 我们知道,RecyclerView是经典的ListView的进化与升华,它比ListView更加灵活,但也因此引入了一定的复杂性.最新的v7支持包新添加了RecyclerView. 我们知道,ListView通过使用ViewHolder来提升性能.ViewHolder通过保存item中使用到的控件的引用来减少findViewById的调用,以此使ListView滑动得更加顺畅.但

解决UITableView中Cell重用机制导致内容出错的方法总结

UITableView继承自UIScrollview,是苹果为我们封装好的一个基于scroll的控件.上面主要是一个个的 UITableViewCell,可以让UITableViewCell响应一些点击事件,也可以在UITableViewCell中加入 UITextField或者UITextView等子视图,使得可以在cell上进行文字编辑. UITableView中的cell可以有很多,一般会通过重用cell来达到节省内存的目的:通过为每个cell指定一个重用标识符 (reuseIdentif

UITableViewCell的重用机制和解决方法

UITableView为了做到显示与数据的分离, 单独使用了一个叫UITableViewCell的视图用来显示每一行的数据, 而tableView得重用机制就是每次只创建屏幕显示区域内的cell,通过重用标识符identifier来标记cell, 当cell要从屏幕外移入屏幕内时, 系统会从重用池内找到相同标识符的cell, 然后拿来显示, 这样本是为了减少过大的内存使用, 但在很多时候, 我们会自定义cell,这时就会出现一些我们不愿意看到的现象, 下面就介绍一些解决这些问题的方法 UITab

UITableViewCell中cell重用机制导致内容重复的方法

UITableView继承自UIScrollview,是苹果为我们封装好的一个基于scroll的控件.上面主要是一个个的UITableViewCell,可以让UITableViewCell响应一些点击事件,也可以在UITableViewCell中加入UITextField或者UITextView等子视图,使得可以在cell上进行文字编辑. UITableView中的cell可以有很多,一般会通过重用cell来达到节省内存的目的:通过为每个cell指定一个重用标识符(reuseIdentifier

Android性能优化之Listview(ViewHolder重用机制)

相信大家在很多时候都会用到ListView这个控件,因为确实是用的很多很多,但是有木有遇到过当数据很多很多的时候,往下滑ListView时有时候会卡顿,这就需要我们来优化它了. ListView优化主要有下面几个方面: 1.convertView重用 2.ViewHolder的子View复用 3.缓存数据复用 一.convertView重用 首先讲下ListView的原理:ListView中的每一个Item显示都需要Adapter调用一次getView()的方法,这个方法会传入一个convert

图片加载库Glide——解决图片错乱+无法设置tag

今天在写一个图片加载类ImageLoader,在使用的时候想用Glide替代我写的ImageLoader,然后问题就出来了!!! 第一个问题:在使用自己写的ImageLoader的时候,为了防止item复用导致的图片错乱,设置了Tag 如下代码 @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (convertView ==

让App中加入LruCache缓存,轻松解决图片过多造成的OOM

上次有过电话面试中问到Android中的缓存策略,当时模糊不清的回答,现在好好理一下吧. Android中一般情况下采取的缓存策略是使用二级缓存,即内存缓存+硬盘缓存->LruCache+DiskLruCache,二级缓存可以满足大部分的需求了,另外还有个三级缓存(内存缓存+硬盘缓存+网络缓存),其中DiskLruCache就是硬盘缓存,下篇再讲吧! 1.那么LruCache到底是什么呢? 查了下官方资料,是这样定义的: LruCache 是对限定数量的缓存对象持有强引用的缓存,每一次缓存对象被

让App中增加LruCache缓存,轻松解决图片过多造成的OOM

上次有过电话面试中问到Android中的缓存策略,当时模糊不清的回答,如今好好理一下吧. Android中普通情况下採取的缓存策略是使用二级缓存.即内存缓存+硬盘缓存->LruCache+DiskLruCache.二级缓存能够满足大部分的需求了,另外还有个三级缓存(内存缓存+硬盘缓存+网络缓存),当中DiskLruCache就是硬盘缓存,下篇再讲吧! 1.那么LruCache究竟是什么呢? 查了下官方资料.是这样定义的: LruCache 是对限定数量的缓存对象持有强引用的缓存,每一次缓存对象被

基于Oracle数据库锁机制,解决集群中的并发访问问题

1.需求 应用场景是这样的: 使用Oracle数据保存待办任务,使用状态字段区分任务是否已经被执行.多个Worker线程同时执行任务,执行成功或失败后,修改状态字段的值. 假设数据库表结构如下所示. create table Task( id varchar2(32), name varchar2(32), flag varchar2(1), worker varchar2(32) ); flag 可取的值包括:0-待办,1-已办,-1-失败待重试. 需要避免的问题: 多个Worker同时工作时