转载于csdn苦逼程序员的笔记博客

对于使用ViewHolder引起的图片错乱问题,相信大部分人都有遇到过,我也一样,对于解决方法也有所了解,但一直都是知其然不知其所以然。

所以,这次直接把ViewHolder的工作原理,通过简单的demo代码来验证一次,验证后对于图片错乱和闪烁这种问题的成因就很清楚了。

下面先上一副图

这幅图就比较清晰的画出了ViewHolder的工作原理。

可以看到,图中左上角item1上面有一条蓝色的线,item7下面也有一条蓝色的线,这两条线就是屏幕的上下边缘,我们在屏幕中能看到的内容就是item1~item7。

当我们控制屏幕向下滚动时,屏幕上的变化是,item1离开了屏幕,紧接着item8进入了屏幕,这是我们看到的。在item1离开,item8进入的过程中,还有一个我们看不到的过程。当item1离开屏幕时,它会进入Recycler(反复循环器)构件,然后被放到了item8的位置,成为了我们看到的item8。

通过代码来验证这个变化过程

下面是MainActivity的代码

初始化了12条数据( 这真的是正经数据 ╮( ̄▽ ̄”)╭ )

初始化Adapter并设置到RecyclerView

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

public class MainActivityextends AppCompatActivity {

private RecyclerView recyclerView;

private List 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。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

public class MyRecyclerAdapterextends RecyclerView.Adapter {

private static final String TAG ="MyRecyclerAdapter";

private List mData;

private Context mContext;

private LayoutInflater inflater;

public MyRecyclerAdapter(Context context, List 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 MyViewHolderextends 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的重用机制,接下来看一段会出现图片错乱的代码示例。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

public class MyRecyclerAdapterextends RecyclerView.Adapter {

private static final String TAG ="MyRecyclerAdapter";

private List mData;

private Context mContext;

private LayoutInflater inflater;

public MyRecyclerAdapter(Context context, List 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() {

@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 MyViewHolderextends 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代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

public class MyRecyclerAdapterextends RecyclerView.Adapter {

private static final String TAG ="MyRecyclerAdapter";

private List mData;

private Context mContext;

private LayoutInflater inflater;

public MyRecyclerAdapter(Context context, List 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() {

@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 MyViewHolderextends RecyclerView.ViewHolder {

ImageView imageView;

public MyViewHolder(View view) {

super(view);

imageView = (ImageView) view.findViewById(R.id.id_img);

}

}

}

解决方式2 demo代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

public class MyRecyclerAdapterextends RecyclerView.Adapter {

private static final String TAG ="MyRecyclerAdapter";

private List mData;

private Context mContext;

private LayoutInflater inflater;

public MyRecyclerAdapter(Context context, List 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() {

@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 MyViewHolderextends 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参数。

reclyview 复用机制_RecyclerView中ViewHolder重用机制理解(解决图片错乱和闪烁问题)...相关推荐

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

    对于使用ViewHolder引起的图片错乱问题,相信大部分人都有遇到过,我也一样,对于解决方法也有所了解,但一直都是知其然不知其所以然. 所以,这次直接把ViewHolder的工作原理,通过简单的de ...

  2. RecyclerView中ViewHolder重用机制理解,解决网络图片错乱和闪烁问题

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

    UITableView继承自UIScrollview,是苹果为我们封装好的一个基于scroll的控件.上面主要是一个个的UITableViewCell,可以让UITableViewCell响应一些点击 ...

  4. android系统的alarm机制,Android中Alarm的机制

    本次给大家分析的是Android中Alarm的机制所用源码为最新的Android4.4.4.首先简单介绍如何使用Alarm并给出其工作原理,接着分析Alarm和Timer以及Handler在完成定时任 ...

  5. linux io复用命令,Linux中IO多路复用机制

    之前的面试有问到主线程在 ActivityThread 里初始化 Looper 后调用了 Looper.loop() 这个死循环为什么不会阻塞主线程,当时回答因为在 Looper.loop() 方法里 ...

  6. java 泛型机制_java中的泛型机制

    泛型 这种语法机制,只在程序编译阶段起作用,只是给编译器参考的.(运行阶段泛型没用) 使用了泛型好处是什么? 第一:集合中存储的元素类型统一. 第二:从集合中取出的元素类型是泛型指定的类型,不需要进行 ...

  7. python的内存机制_python中的内存机制

    首先要明白对象和引用的概念 (例子:a=1, a为引用,1为对象,对象1的引用计数器为1,b=1此时内存中只有一个对象1,a,b都为引用,对象的引用计数器此时为2,因为有两个引用) a=1,b=1id ...

  8. iphone 重用机制

    今天在研究SDWebImage和ASIHTTPRequest实现网络图片异步加载和本地缓存的时候,在UITableView显示图片的时候,出现了一些奇异的现象,比如: 1.TableView一次只能显 ...

  9. CV中的Attention机制总结

    CV中的Attention机制 注意力机制 CV中的注意力机制 卷积神经网络中常用的Attention 视觉注意力机制在分类网络中的应用 SE-Net(CVPR 2017) ECA-Net(CVPR ...

最新文章

  1. android列表实现置顶,Android利用RecyclerView实现全选、置顶和拖拽功能示例
  2. haproxy LVS nginx的比较
  3. 一个IO的传奇一生(8) -- elevator子系统
  4. Netty入门(七)使用SSL/TLS加密Netty程序
  5. ajax webservice 参数类型,JQuery Ajax WebService传递参数的简单实例
  6. [MATLAB调试笔记]Update the electric field in one step
  7. 代码单元测试工具:gmock
  8. centos 5.8 升级php5.1至5.3
  9. 深入研究.NET Core的本地化机制
  10. 老师:你根本不知道我有多想逃课
  11. 断开的管道 java.io.IOException: Broken pipe 解决方法
  12. jar包导出无法显示图片或者音乐_如何制作图片视频短片,配上音乐闪耀朋友圈!...
  13. 信息学奥赛一本通C++语言——1057:简单计算器
  14. Linux第二周考试题
  15. springsecurity权限过滤略解
  16. Allegro 拼板
  17. 传统的企业如何实现数字化转型?
  18. 【C语言程序设计·考试复习】视频讲解课程合集
  19. jQuery 3d云标签
  20. 北京大学MOOC 程序设计与算法(三)C++面向对象程序设计 期末考试

热门文章

  1. 抓取html表单验证码,jquery实现表单获取短信验证码代码
  2. 【云简评】之六《2015金山云战略解析》
  3. Centos7安装和配置VNC服务器 - openbox篇
  4. P2031 脑力达人之分割字串
  5. 密码破解很容易,这是怎么做的
  6. python分析人口出生率代码_身份证号码各位数字的含义以及计算校验位的python代码...
  7. (2017.8.14更新)CnCrypt加密U盘1.22,将U盘划分为普通盘和加密盘,支持与U盘启动盘共存
  8. docx poi 原理,如何从Apache POI知道文件是.docx还是.doc格式
  9. 开章——2013电赛
  10. 普遍使用的自动报靶系统有哪些优缺点