Android中复用问题哲理性解析
Android中列表的复用机制提高了APP的运行效率,但随之而来的复用的问题总是让程序员们头痛,一个 bug找头天也找不到。我就把自己解决这方面的经验贡献出来供大家参考: 问题1:什么是复用 复用其实指的是复用View,而绑定View的数据是变化的。 问题2:复用的原理探究 为了彻底弄清楚复用的原理,和特地写了段小程序。 Adapter代码: 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 class MyAdapter extends BaseAdapter{ @Override public int getCount(){ return 20 ; } @Override public ObjectgetItem( int position){ return null ; } @Override public long getItemId( int position){ return position; } @Override public ViewgetView( int position,ViewconvertView,ViewGroupparent){ Log.i(TAG, "aaaaaaaaaa----------getView:position=" +position+ ",convertView=" +convertView); ViewHolderholder; if (convertView== null ){ convertView=LayoutInflater.from(SimpleCheckBoxListActivity. this ).inflate(R.layout.adapter_simple_checkbox_item, null , false ); holder= new ViewHolder(); holder.tv=(TextView)convertView.findViewById(R.id.tv); holder.cb=(CheckBox)convertView.findViewById(R.id.cb); convertView.setTag(holder); } else { holder=(ViewHolder)convertView.getTag(); } holder.tv.setText( "index=" +position); Log.i(TAG, "bbbbbbbbbb----------getView:position=" +position+ ",convertView=" +convertView.toString()); //将convertView缓存起来,方便后面的分析。 itemViews.put(position,convertView); //分析当前position是否复用了之前哪个位置的view int reusePosition=analyseReusedWhichPosition(position); if (reusePosition!=- 1 ){ Log.i(TAG, "getView:位置" +position+ "复用了位置" +reusePosition+ "的view" ); } return convertView; } class ViewHolder{ TextViewtv; CheckBoxcb; } //分析当前position是否复用了之前哪个位置的view private int analyseReusedWhichPosition( int currentPosition){ ViewcurrentPositionView=itemViews.get(currentPosition); for ( int i= 0 ;i<currentPosition;i++){ ViewbeforePositionView=itemViews.get(i); if (beforePositionView== null ){ continue ; } if (beforePositionView==currentPositionView){ return i; } } return - 1 ; } } 日志分析: 1)程序初次运行 打印的日志: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 aaaaaaaaaa----------getView:position= 0 ,convertView= null bbbbbbbbbb----------getView:position= 0 ,convertView=android.widget.LinearLayout{42eceab0V.E...........I. 0 , 0 - 0 , 0 } aaaaaaaaaa----------getView:position= 1 ,convertView= null bbbbbbbbbb----------getView:position= 1 ,convertView=android.widget.LinearLayout{42ee4650V.E...........I. 0 , 0 - 0 , 0 } aaaaaaaaaa----------getView:position= 2 ,convertView= null bbbbbbbbbb----------getView:position= 2 ,convertView=android.widget.LinearLayout{42ee6140V.E...........I. 0 , 0 - 0 , 0 } aaaaaaaaaa----------getView:position= 3 ,convertView= null bbbbbbbbbb----------getView:position= 3 ,convertView=android.widget.LinearLayout{42ee7c10V.E...........I. 0 , 0 - 0 , 0 } aaaaaaaaaa----------getView:position= 4 ,convertView= null bbbbbbbbbb----------getView:position= 4 ,convertView=android.widget.LinearLayout{42ee96e0V.E...........I. 0 , 0 - 0 , 0 } aaaaaaaaaa----------getView:position= 5 ,convertView= null bbbbbbbbbb----------getView:position= 5 ,convertView=android.widget.LinearLayout{42eeb1e8V.E...........I. 0 , 0 - 0 , 0 } aaaaaaaaaa----------getView:position= 6 ,convertView= null bbbbbbbbbb----------getView:position= 6 ,convertView=android.widget.LinearLayout{42eeccb8V.E...........I. 0 , 0 - 0 , 0 } aaaaaaaaaa----------getView:position= 7 ,convertView= null bbbbbbbbbb----------getView:position= 7 ,convertView=android.widget.LinearLayout{42eee788V.E...........I. 0 , 0 - 0 , 0 } 2)接着向下滑动,索引0没有完全消失,索引8就出现了,这时还没有复用。 打印的日志: 1 2 aaaaaaaaaa----------getView:position= 8 ,convertView= null bbbbbbbbbb----------getView:position= 8 ,convertView=android.widget.LinearLayout{42ef5150V.E...........I. 0 , 0 - 0 , 0 } 3)位置9出现,索引0已经完全消失(复用开始出现) 打印的日志: 1 2 3 4 aaaaaaaaaa----------getView:position= 9 ,convertView=android.widget.LinearLayout{42eceab0V.E............. 0 ,- 215 - 1080 , 1 } bbbbbbbbbb----------getView:position= 9 ,convertView=android.widget.LinearLayout{42eceab0V.E............D 0 ,- 215 - 1080 , 1 } getView:位置 9 复用了位置 0 的view 可以发现索引 9 处的hashCode与索引 0 处的hashCode都是42eceab0 4)紧接着向下滚动到最后(注意是慢慢地滚动) 打印的日志: 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 aaaaaaaaaa----------getView:position= 10 ,convertView=android.widget.LinearLayout{42ee4650V.E............. 0 ,- 213 - 1080 , 3 } bbbbbbbbbb----------getView:position= 10 ,convertView=android.widget.LinearLayout{42ee4650V.E............D 0 ,- 213 - 1080 , 3 } getView:位置 10 复用了位置 1 的view aaaaaaaaaa----------getView:position= 11 ,convertView=android.widget.LinearLayout{42ee6140V.E............. 0 ,- 205 - 1080 , 11 } bbbbbbbbbb----------getView:position= 11 ,convertView=android.widget.LinearLayout{42ee6140V.E............D 0 ,- 205 - 1080 , 11 } getView:位置 11 复用了位置 2 的view aaaaaaaaaa----------getView:position= 12 ,convertView=android.widget.LinearLayout{42ee7c10V.E............. 0 ,- 202 - 1080 , 14 } bbbbbbbbbb----------getView:position= 12 ,convertView=android.widget.LinearLayout{42ee7c10V.E............D 0 ,- 202 - 1080 , 14 } getView:位置 12 复用了位置 3 的view aaaaaaaaaa----------getView:position= 13 ,convertView=android.widget.LinearLayout{42ee96e0V.E............. 0 ,- 201 - 1080 , 15 } bbbbbbbbbb----------getView:position= 13 ,convertView=android.widget.LinearLayout{42ee96e0V.E............D 0 ,- 201 - 1080 , 15 } getView:位置 13 复用了位置 4 的view aaaaaaaaaa----------getView:position= 14 ,convertView=android.widget.LinearLayout{42eeb1e8V.E............. 0 ,- 188 - 1080 , 28 } bbbbbbbbbb----------getView:position= 14 ,convertView=android.widget.LinearLayout{42eeb1e8V.E............D 0 ,- 188 - 1080 , 28 } getView:位置 14 复用了位置 5 的view aaaaaaaaaa----------getView:position= 15 ,convertView=android.widget.LinearLayout{42eeccb8V.E............. 0 ,- 213 - 1080 , 3 } bbbbbbbbbb----------getView:position= 15 ,convertView=android.widget.LinearLayout{42eeccb8V.E............D 0 ,- 213 - 1080 , 3 } getView:位置 15 复用了位置 6 的view aaaaaaaaaa----------getView:position= 16 ,convertView=android.widget.LinearLayout{42eee788V.E............. 0 ,- 179 - 1080 , 37 } bbbbbbbbbb----------getView:position= 16 ,convertView=android.widget.LinearLayout{42eee788V.E............D 0 ,- 179 - 1080 , 37 } getView:位置 16 复用了位置 7 的view aaaaaaaaaa----------getView:position= 17 ,convertView=android.widget.LinearLayout{42ef5150V.E............. 0 ,- 181 - 1080 , 35 } bbbbbbbbbb----------getView:position= 17 ,convertView=android.widget.LinearLayout{42ef5150V.E............D 0 ,- 181 - 1080 , 35 } getView:位置 17 复用了位置 8 的view aaaaaaaaaa----------getView:position= 18 ,convertView=android.widget.LinearLayout{42eceab0V.E............. 0 ,- 195 - 1080 , 21 } bbbbbbbbbb----------getView:position= 18 ,convertView=android.widget.LinearLayout{42eceab0V.E............D 0 ,- 195 - 1080 , 21 } getView:位置 18 复用了位置 0 的view aaaaaaaaaa----------getView:position= 19 ,convertView=android.widget.LinearLayout{42ee4650V.E............. 0 ,- 210 - 1080 , 6 } bbbbbbbbbb----------getView:position= 19 ,convertView=android.widget.LinearLayout{42ee4650V.E............D 0 ,- 210 - 1080 , 6 } getView:位置 19 复用了位置 1 的view 可以看到向下慢慢滑动的时候,复用是很有规律的。 但是如果快速的向下滑动的时候,又发现不了什么规律:复用并非是连续的 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 aaaaaaaaaa----------getView:position= 8 ,convertView=android.widget.LinearLayout{42f9a780V.E............. 0 ,- 85 - 1080 , 131 } bbbbbbbbbb----------getView:position= 8 ,convertView=android.widget.LinearLayout{42f9a780V.E............D 0 ,- 85 - 1080 , 131 } getView:位置 8 复用了位置 0 的view aaaaaaaaaa----------getView:position= 9 ,convertView=android.widget.LinearLayout{42f9f818V.E............. 0 , 384 - 1080 , 600 } bbbbbbbbbb----------getView:position= 9 ,convertView=android.widget.LinearLayout{42f9f818V.E............D 0 , 384 - 1080 , 600 } getView:位置 9 复用了位置 3 的view aaaaaaaaaa----------getView:position= 10 ,convertView=android.widget.LinearLayout{42f9dd48V.E............. 0 , 138 - 1080 , 354 } bbbbbbbbbb----------getView:position= 10 ,convertView=android.widget.LinearLayout{42f9dd48V.E............D 0 , 138 - 1080 , 354 } getView:位置 10 复用了位置 2 的view aaaaaaaaaa----------getView:position= 11 ,convertView=android.widget.LinearLayout{42f9c278V.E............. 0 ,- 108 - 1080 , 108 } bbbbbbbbbb----------getView:position= 11 ,convertView=android.widget.LinearLayout{42f9c278V.E............D 0 ,- 108 - 1080 , 108 } getView:位置 11 复用了位置 1 的view aaaaaaaaaa----------getView:position= 12 ,convertView= null bbbbbbbbbb----------getView:position= 12 ,convertView=android.widget.LinearLayout{42fad3a0V.E...........I. 0 , 0 - 0 , 0 } aaaaaaaaaa----------getView:position= 13 ,convertView=android.widget.LinearLayout{42fa2df0V.E............. 0 , 60 - 1080 , 276 } bbbbbbbbbb----------getView:position= 13 ,convertView=android.widget.LinearLayout{42fa2df0V.E............D 0 , 60 - 1080 , 276 } getView:位置 13 复用了位置 5 的view aaaaaaaaaa----------getView:position= 14 ,convertView=android.widget.LinearLayout{42fa12e8V.E............. 0 ,- 186 - 1080 , 30 } bbbbbbbbbb----------getView:position= 14 ,convertView=android.widget.LinearLayout{42fa12e8V.E............D 0 ,- 186 - 1080 , 30 } getView:位置 14 复用了位置 4 的view aaaaaaaaaa----------getView:position= 15 ,convertView=android.widget.LinearLayout{42fa48c0V.E............. 0 ,- 150 - 1080 , 66 } bbbbbbbbbb----------getView:position= 15 ,convertView=android.widget.LinearLayout{42fa48c0V.E............D 0 ,- 150 - 1080 , 66 } getView:位置 15 复用了位置 6 的view aaaaaaaaaa----------getView:position= 16 ,convertView=android.widget.LinearLayout{42f9a780V.E............. 0 , 78 - 1080 , 294 } bbbbbbbbbb----------getView:position= 16 ,convertView=android.widget.LinearLayout{42f9a780V.E............D 0 , 78 - 1080 , 294 } getView:位置 16 复用了位置 0 的view aaaaaaaaaa----------getView:position= 17 ,convertView=android.widget.LinearLayout{42f9f818V.E............. 0 , 13 - 1080 , 229 } bbbbbbbbbb----------getView:position= 17 ,convertView=android.widget.LinearLayout{42f9f818V.E............D 0 , 13 - 1080 , 229 } getView:位置 17 复用了位置 3 的view aaaaaaaaaa----------getView:position= 18 ,convertView=android.widget.LinearLayout{42f9dd48V.E............. 0 ,- 28 - 1080 , 188 } bbbbbbbbbb----------getView:position= 18 ,convertView=android.widget.LinearLayout{42f9dd48V.E............D 0 ,- 28 - 1080 , 188 } getView:位置 18 复用了位置 2 的view aaaaaaaaaa----------getView:position= 19 ,convertView=android.widget.LinearLayout{42fa6390V.E............. 0 ,- 168 - 1080 , 48 } bbbbbbbbbb----------getView:position= 19 ,convertView=android.widget.LinearLayout{42fa6390V.E............D 0 ,- 168 - 1080 , 48 } getView:位置 19 复用了位置 7 的view 5)最后,向上滚动到索引为0的位置 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 aaaaaaaaaa----------getView:position= 11 ,convertView=android.widget.LinearLayout{4304de70V.E............. 0 ,- 212 - 1080 , 4 } bbbbbbbbbb----------getView:position= 11 ,convertView=android.widget.LinearLayout{4304de70V.E............D 0 ,- 212 - 1080 , 4 } getView:位置 11 复用了位置 8 的view aaaaaaaaaa----------getView:position= 10 ,convertView=android.widget.LinearLayout{4303ee70V.E............. 0 , 1829 - 1080 , 2045 } bbbbbbbbbb----------getView:position= 10 ,convertView=android.widget.LinearLayout{4303ee70V.E............D 0 , 1829 - 1080 , 2045 } getView:位置 10 复用了位置 1 的view aaaaaaaaaa----------getView:position= 9 ,convertView=android.widget.LinearLayout{ 43040940 V.E............. 0 , 1829 - 1080 , 2045 } bbbbbbbbbb----------getView:position= 9 ,convertView=android.widget.LinearLayout{ 43040940 V.E............D 0 , 1829 - 1080 , 2045 } getView:位置 9 复用了位置 2 的view aaaaaaaaaa----------getView:position= 8 ,convertView=android.widget.LinearLayout{4303d378V.E............. 0 , 1830 - 1080 , 2046 } bbbbbbbbbb----------getView:position= 8 ,convertView=android.widget.LinearLayout{4303d378V.E............D 0 , 1830 - 1080 , 2046 } getView:位置 8 复用了位置 0 的view aaaaaaaaaa----------getView:position= 7 ,convertView=android.widget.LinearLayout{430474b8V.E............. 0 , 1825 - 1080 , 2041 } bbbbbbbbbb----------getView:position= 7 ,convertView=android.widget.LinearLayout{430474b8V.E............D 0 , 1825 - 1080 , 2041 } getView:位置 7 复用了位置 6 的view aaaaaaaaaa----------getView:position= 6 ,convertView=android.widget.LinearLayout{43048f88V.E............. 0 , 1824 - 1080 , 2040 } bbbbbbbbbb----------getView:position= 6 ,convertView=android.widget.LinearLayout{43048f88V.E............D 0 , 1824 - 1080 , 2040 } aaaaaaaaaa----------getView:position= 5 ,convertView=android.widget.LinearLayout{43043ee0V.E............. 0 , 1822 - 1080 , 2038 } bbbbbbbbbb----------getView:position= 5 ,convertView=android.widget.LinearLayout{43043ee0V.E............D 0 , 1822 - 1080 , 2038 } getView:位置 5 复用了位置 4 的view aaaaaaaaaa----------getView:position= 4 ,convertView=android.widget.LinearLayout{430459e8V.E............. 0 , 1823 - 1080 , 2039 } bbbbbbbbbb----------getView:position= 4 ,convertView=android.widget.LinearLayout{430459e8V.E............D 0 , 1823 - 1080 , 2039 } aaaaaaaaaa----------getView:position= 3 ,convertView=android.widget.LinearLayout{ 43042410 V.E............. 0 , 1829 - 1080 , 2045 } bbbbbbbbbb----------getView:position= 3 ,convertView=android.widget.LinearLayout{ 43042410 V.E............D 0 , 1829 - 1080 , 2045 } aaaaaaaaaa----------getView:position= 2 ,convertView=android.widget.LinearLayout{4304de70V.E............. 0 , 1826 - 1080 , 2042 } bbbbbbbbbb----------getView:position= 2 ,convertView=android.widget.LinearLayout{4304de70V.E............D 0 , 1826 - 1080 , 2042 } aaaaaaaaaa----------getView:position= 1 ,convertView=android.widget.LinearLayout{4303ee70V.E............. 0 , 1828 - 1080 , 2044 } bbbbbbbbbb----------getView:position= 1 ,convertView=android.widget.LinearLayout{4303ee70V.E............D 0 , 1828 - 1080 , 2044 } aaaaaaaaaa----------getView:position= 0 ,convertView=android.widget.LinearLayout{ 43040940 V.E............. 0 , 1788 - 1080 , 2004 } bbbbbbbbbb----------getView:position= 0 ,convertView=android.widget.LinearLayout{ 43040940 V.E............D 0 , 1788 - 1080 , 2004 } 如上所述,到底谁复用了谁是随机不定的,这个我们也没有必要去关心。我们只要知道position是不变的就行了。 另外,除了打日志。可以选中某一个位置的checkbox,然后上下滑动,如果某个checkbox也莫名的选中了,那就说明这个位置的checkbox复用了之前选中的那个checkbox。 问题3:Adapter的notifyDataSetChanged()方法作了什么事情 notifyDataSetChanged,会重新走一遍可见的position的getView方法。 问题4:复用出现的场景 1.if-else的坑:在Adapter中,如果绑定View的数据的时候如果有if判断,往往很多人忘记了加else,这是大多数复用问题出现的根源之一。在一般情况下else不写没有逻辑错误,但是在ListView复 用的情况下如果不写错误就会带来错乱的麻烦。 实际场景: 比如每个item可能有或没有图片picarrList,之前我只加了if判断,如果有图片就显示。但后来上下一滑动之后发现没有图片的item竟然也显示了其它了item的图片,于是追根溯源发现是这里的问题。 2.checkbox等的复用问题:果如下图,是一个简单的CheckBox列表 第1页刚好0-8索引,我将0索引处的checkbox设置为选中状态,然后向下滑动,发现下一个出现的checkbox(索引为10,不是9,也不一定就是10,而是索引0完全消失之后第一个出现的item)竟然也选中了。 百度了一下,可以用Map<Interger,Boolean>来记录对应position的checkbox的选中状态。而且网上 的这个Map是事先就是预订好大小的了,但实际中Map的大小是确定的。 细节1):Map<Interger,Boolean>来记录对应position的checkbox的选中状态,怎么初始化? --1-- 可以先在成员或者构造方法里实例化Map对象 1 Map<Integer,Boolean>isTitleCheckBoxSelected= new HashMap(); --2-- 在getView方法里初始化Map对象,默认checkbox都是未选中状态 1 2 3 4 5 6 7 8 if (!isTitleCheckBoxSelected.containsKey(position)){ Log.i(TAG, "bindData:initcheckbox" +position); isTitleCheckBoxSelected.put(position, false ); //如果启动了全选,则新出现的view也要选中。 if (isSelectedAllStarted){ isTitleCheckBoxSelected.put(position, true ); } } 上面的这段代码其实是非常妙的,通过contains判断,保证了初始化。如果后面操作了map, 也不会影响这段代码对map的初始化。 map这种数据结构,由于key是唯一,可以做去重操作。这一点List则不可直接做到。 细节2):响应checkbox的OnCheckedChangeListener事件,将改变后的状态保存到map中。 1 2 3 4 5 6 7 header_checkbox.setOnCheckedChangeListener( new CompoundButton.OnCheckedChangeListener(){ @Override public void onCheckedChanged(CompoundButtonbuttonView, boolean isChecked){ if (isCheckedByCode) return ; isTitleCheckBoxSelected.put(position,!isTitleCheckBoxSelected.get(position)); } }); 在onCheckedChanged方法里将对应position的checkbox的状态反转。 细节3):将map中的对应position的状态值赋值给当前的checkbox 但是有个问题,checkbox的setChecked方法,看其源码,会走OnCheckedChangeListener的回调 而此时,setChecked方法我只想设置View的状态,并不想走它的回调方法。下面有2种方法可以解决这个问题 方法1:在setChecked方法的前后用一个变量夹住,在回调方法里通过这个变量判断回调是不是在代码 里通过setChecked触发,如果是setChecked触发的,则不执行map的取反的操作。 1 2 3 isCheckedByCode= true ; header_checkbox.setChecked(isTitleCheckBoxSelected.get(position)); isCheckedByCode= false ; 这种方法多申请了个变量,耦合度比较高。 方法2:在setChecked方法之前将checkbox的监听设置为null,在setChecked方法之后设置真正的监听。 除了checkbox,其它的一些view,也可以通过以上的方法来解决复用的问题。解决复用要遵循一个原则:MV分离,在view一些事件监听里,一般情况下改变记录状态的Map值之后,切记立马就将值设置给View,而应该通过notifyDatasetChanged()方法将状态更新到view上。 本文转自屠夫章哥 51CTO博客,原文链接:http://blog.51cto.com/4259297/1846668,如需转载请自行联系原作者