Android中复用问题哲理性解析
Android中列表的复用机制提高了APP的运行效率,但随之而来的复用的问题总是让程序员们头痛,一个
bug找头天也找不到。我就把自己解决这方面的经验贡献出来供大家参考:
问题1:什么是复用
复用其实指的是复用View,而绑定View的数据是变化的。
问题2:复用的原理探究
为了彻底弄清楚复用的原理,和特地写了段小程序。
Adapter代码:
classMyAdapterextendsBaseAdapter{@OverridepublicintgetCount(){return20;}@OverridepublicObjectgetItem(intposition){returnnull;}@OverridepubliclonggetItemId(intposition){returnposition;}@OverridepublicViewgetView(intposition,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=newViewHolder();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是否复用了之前哪个位置的viewintreusePosition=analyseReusedWhichPosition(position);if(reusePosition!=-1){Log.i(TAG,"getView:位置"+position+"复用了位置"+reusePosition+"的view");}returnconvertView;}classViewHolder{TextViewtv;CheckBoxcb;}//分析当前position是否复用了之前哪个位置的viewprivateintanalyseReusedWhichPosition(intcurrentPosition){ViewcurrentPositionView=itemViews.get(currentPosition);for(inti=0;i<currentPosition;i++){ViewbeforePositionView=itemViews.get(i);if(beforePositionView==null){continue;}if(beforePositionView==currentPositionView){returni;}}return-1;}}
日志分析:
1)程序初次运行
打印的日志:
aaaaaaaaaa----------getView:position=0,convertView=nullbbbbbbbbbb----------getView:position=0,convertView=android.widget.LinearLayout{42eceab0V.E...........I.0,0-0,0}aaaaaaaaaa----------getView:position=1,convertView=nullbbbbbbbbbb----------getView:position=1,convertView=android.widget.LinearLayout{42ee4650V.E...........I.0,0-0,0}aaaaaaaaaa----------getView:position=2,convertView=nullbbbbbbbbbb----------getView:position=2,convertView=android.widget.LinearLayout{42ee6140V.E...........I.0,0-0,0}aaaaaaaaaa----------getView:position=3,convertView=nullbbbbbbbbbb----------getView:position=3,convertView=android.widget.LinearLayout{42ee7c10V.E...........I.0,0-0,0}aaaaaaaaaa----------getView:position=4,convertView=nullbbbbbbbbbb----------getView:position=4,convertView=android.widget.LinearLayout{42ee96e0V.E...........I.0,0-0,0}aaaaaaaaaa----------getView:position=5,convertView=nullbbbbbbbbbb----------getView:position=5,convertView=android.widget.LinearLayout{42eeb1e8V.E...........I.0,0-0,0}aaaaaaaaaa----------getView:position=6,convertView=nullbbbbbbbbbb----------getView:position=6,convertView=android.widget.LinearLayout{42eeccb8V.E...........I.0,0-0,0}aaaaaaaaaa----------getView:position=7,convertView=nullbbbbbbbbbb----------getView:position=7,convertView=android.widget.LinearLayout{42eee788V.E...........I.0,0-0,0}
2)接着向下滑动,索引0没有完全消失,索引8就出现了,这时还没有复用。
打印的日志:
aaaaaaaaaa----------getView:position=8,convertView=nullbbbbbbbbbb----------getView:position=8,convertView=android.widget.LinearLayout{42ef5150V.E...........I.0,0-0,0}
3)位置9出现,索引0已经完全消失(复用开始出现)
打印的日志:
aaaaaaaaaa----------getView:position=9,convertView=android.widget.LinearLayout{42eceab0V.E.............0,-215-1080,1}bbbbbbbbbb----------getView:position=9,convertView=android.widget.LinearLayout{42eceab0V.E............D0,-215-1080,1}getView:位置9复用了位置0的view可以发现索引9处的hashCode与索引0处的hashCode都是42eceab0
4)紧接着向下滚动到最后(注意是慢慢地滚动)
打印的日志:
aaaaaaaaaa----------getView:position=10,convertView=android.widget.LinearLayout{42ee4650V.E.............0,-213-1080,3}bbbbbbbbbb----------getView:position=10,convertView=android.widget.LinearLayout{42ee4650V.E............D0,-213-1080,3}getView:位置10复用了位置1的viewaaaaaaaaaa----------getView:position=11,convertView=android.widget.LinearLayout{42ee6140V.E.............0,-205-1080,11}bbbbbbbbbb----------getView:position=11,convertView=android.widget.LinearLayout{42ee6140V.E............D0,-205-1080,11}getView:位置11复用了位置2的viewaaaaaaaaaa----------getView:position=12,convertView=android.widget.LinearLayout{42ee7c10V.E.............0,-202-1080,14}bbbbbbbbbb----------getView:position=12,convertView=android.widget.LinearLayout{42ee7c10V.E............D0,-202-1080,14}getView:位置12复用了位置3的viewaaaaaaaaaa----------getView:position=13,convertView=android.widget.LinearLayout{42ee96e0V.E.............0,-201-1080,15}bbbbbbbbbb----------getView:position=13,convertView=android.widget.LinearLayout{42ee96e0V.E............D0,-201-1080,15}getView:位置13复用了位置4的viewaaaaaaaaaa----------getView:position=14,convertView=android.widget.LinearLayout{42eeb1e8V.E.............0,-188-1080,28}bbbbbbbbbb----------getView:position=14,convertView=android.widget.LinearLayout{42eeb1e8V.E............D0,-188-1080,28}getView:位置14复用了位置5的viewaaaaaaaaaa----------getView:position=15,convertView=android.widget.LinearLayout{42eeccb8V.E.............0,-213-1080,3}bbbbbbbbbb----------getView:position=15,convertView=android.widget.LinearLayout{42eeccb8V.E............D0,-213-1080,3}getView:位置15复用了位置6的viewaaaaaaaaaa----------getView:position=16,convertView=android.widget.LinearLayout{42eee788V.E.............0,-179-1080,37}bbbbbbbbbb----------getView:position=16,convertView=android.widget.LinearLayout{42eee788V.E............D0,-179-1080,37}getView:位置16复用了位置7的viewaaaaaaaaaa----------getView:position=17,convertView=android.widget.LinearLayout{42ef5150V.E.............0,-181-1080,35}bbbbbbbbbb----------getView:position=17,convertView=android.widget.LinearLayout{42ef5150V.E............D0,-181-1080,35}getView:位置17复用了位置8的viewaaaaaaaaaa----------getView:position=18,convertView=android.widget.LinearLayout{42eceab0V.E.............0,-195-1080,21}bbbbbbbbbb----------getView:position=18,convertView=android.widget.LinearLayout{42eceab0V.E............D0,-195-1080,21}getView:位置18复用了位置0的viewaaaaaaaaaa----------getView:position=19,convertView=android.widget.LinearLayout{42ee4650V.E.............0,-210-1080,6}bbbbbbbbbb----------getView:position=19,convertView=android.widget.LinearLayout{42ee4650V.E............D0,-210-1080,6}getView:位置19复用了位置1的view
可以看到向下慢慢滑动的时候,复用是很有规律的。
但是如果快速的向下滑动的时候,又发现不了什么规律:复用并非是连续的
aaaaaaaaaa----------getView:position=8,convertView=android.widget.LinearLayout{42f9a780V.E.............0,-85-1080,131}bbbbbbbbbb----------getView:position=8,convertView=android.widget.LinearLayout{42f9a780V.E............D0,-85-1080,131}getView:位置8复用了位置0的viewaaaaaaaaaa----------getView:position=9,convertView=android.widget.LinearLayout{42f9f818V.E.............0,384-1080,600}bbbbbbbbbb----------getView:position=9,convertView=android.widget.LinearLayout{42f9f818V.E............D0,384-1080,600}getView:位置9复用了位置3的viewaaaaaaaaaa----------getView:position=10,convertView=android.widget.LinearLayout{42f9dd48V.E.............0,138-1080,354}bbbbbbbbbb----------getView:position=10,convertView=android.widget.LinearLayout{42f9dd48V.E............D0,138-1080,354}getView:位置10复用了位置2的viewaaaaaaaaaa----------getView:position=11,convertView=android.widget.LinearLayout{42f9c278V.E.............0,-108-1080,108}bbbbbbbbbb----------getView:position=11,convertView=android.widget.LinearLayout{42f9c278V.E............D0,-108-1080,108}getView:位置11复用了位置1的viewaaaaaaaaaa----------getView:position=12,convertView=nullbbbbbbbbbb----------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............D0,60-1080,276}getView:位置13复用了位置5的viewaaaaaaaaaa----------getView:position=14,convertView=android.widget.LinearLayout{42fa12e8V.E.............0,-186-1080,30}bbbbbbbbbb----------getView:position=14,convertView=android.widget.LinearLayout{42fa12e8V.E............D0,-186-1080,30}getView:位置14复用了位置4的viewaaaaaaaaaa----------getView:position=15,convertView=android.widget.LinearLayout{42fa48c0V.E.............0,-150-1080,66}bbbbbbbbbb----------getView:position=15,convertView=android.widget.LinearLayout{42fa48c0V.E............D0,-150-1080,66}getView:位置15复用了位置6的viewaaaaaaaaaa----------getView:position=16,convertView=android.widget.LinearLayout{42f9a780V.E.............0,78-1080,294}bbbbbbbbbb----------getView:position=16,convertView=android.widget.LinearLayout{42f9a780V.E............D0,78-1080,294}getView:位置16复用了位置0的viewaaaaaaaaaa----------getView:position=17,convertView=android.widget.LinearLayout{42f9f818V.E.............0,13-1080,229}bbbbbbbbbb----------getView:position=17,convertView=android.widget.LinearLayout{42f9f818V.E............D0,13-1080,229}getView:位置17复用了位置3的viewaaaaaaaaaa----------getView:position=18,convertView=android.widget.LinearLayout{42f9dd48V.E.............0,-28-1080,188}bbbbbbbbbb----------getView:position=18,convertView=android.widget.LinearLayout{42f9dd48V.E............D0,-28-1080,188}getView:位置18复用了位置2的viewaaaaaaaaaa----------getView:position=19,convertView=android.widget.LinearLayout{42fa6390V.E.............0,-168-1080,48}bbbbbbbbbb----------getView:position=19,convertView=android.widget.LinearLayout{42fa6390V.E............D0,-168-1080,48}getView:位置19复用了位置7的view
5)最后,向上滚动到索引为0的位置
aaaaaaaaaa----------getView:position=11,convertView=android.widget.LinearLayout{4304de70V.E.............0,-212-1080,4}bbbbbbbbbb----------getView:position=11,convertView=android.widget.LinearLayout{4304de70V.E............D0,-212-1080,4}getView:位置11复用了位置8的viewaaaaaaaaaa----------getView:position=10,convertView=android.widget.LinearLayout{4303ee70V.E.............0,1829-1080,2045}bbbbbbbbbb----------getView:position=10,convertView=android.widget.LinearLayout{4303ee70V.E............D0,1829-1080,2045}getView:位置10复用了位置1的viewaaaaaaaaaa----------getView:position=9,convertView=android.widget.LinearLayout{43040940V.E.............0,1829-1080,2045}bbbbbbbbbb----------getView:position=9,convertView=android.widget.LinearLayout{43040940V.E............D0,1829-1080,2045}getView:位置9复用了位置2的viewaaaaaaaaaa----------getView:position=8,convertView=android.widget.LinearLayout{4303d378V.E.............0,1830-1080,2046}bbbbbbbbbb----------getView:position=8,convertView=android.widget.LinearLayout{4303d378V.E............D0,1830-1080,2046}getView:位置8复用了位置0的viewaaaaaaaaaa----------getView:position=7,convertView=android.widget.LinearLayout{430474b8V.E.............0,1825-1080,2041}bbbbbbbbbb----------getView:position=7,convertView=android.widget.LinearLayout{430474b8V.E............D0,1825-1080,2041}getView:位置7复用了位置6的viewaaaaaaaaaa----------getView:position=6,convertView=android.widget.LinearLayout{43048f88V.E.............0,1824-1080,2040}bbbbbbbbbb----------getView:position=6,convertView=android.widget.LinearLayout{43048f88V.E............D0,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............D0,1822-1080,2038}getView:位置5复用了位置4的viewaaaaaaaaaa----------getView:position=4,convertView=android.widget.LinearLayout{430459e8V.E.............0,1823-1080,2039}bbbbbbbbbb----------getView:position=4,convertView=android.widget.LinearLayout{430459e8V.E............D0,1823-1080,2039}aaaaaaaaaa----------getView:position=3,convertView=android.widget.LinearLayout{43042410V.E.............0,1829-1080,2045}bbbbbbbbbb----------getView:position=3,convertView=android.widget.LinearLayout{43042410V.E............D0,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............D0,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............D0,1828-1080,2044}aaaaaaaaaa----------getView:position=0,convertView=android.widget.LinearLayout{43040940V.E.............0,1788-1080,2004}bbbbbbbbbb----------getView:position=0,convertView=android.widget.LinearLayout{43040940V.E............D0,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对象
Map<Integer,Boolean>isTitleCheckBoxSelected=newHashMap();
--2-- 在getView方法里初始化Map对象,默认checkbox都是未选中状态
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中。
header_checkbox.setOnCheckedChangeListener(newCompoundButton.OnCheckedChangeListener(){@OverridepublicvoidonCheckedChanged(CompoundButtonbuttonView,booleanisChecked){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的取反的操作。
isCheckedByCode=true;header_checkbox.setChecked(isTitleCheckBoxSelected.get(position));isCheckedByCode=false;
这种方法多申请了个变量,耦合度比较高。
方法2:在setChecked方法之前将checkbox的监听设置为null,在setChecked方法之后设置真正的监听。
除了checkbox,其它的一些view,也可以通过以上的方法来解决复用的问题。解决复用要遵循一个原则:MV分离,在view一些事件监听里,一般情况下改变记录状态的Map值之后,切记立马就将值设置给View,而应该通过notifyDatasetChanged()方法将状态更新到view上。
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。