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
Object getItem(
int
position) {
return
null
;
}
@Override
public
long
getItemId(
int
position) {
return
position;
}
@Override
public
View getView(
int
position, View convertView, ViewGroup parent) {
Log.i(TAG,
"aaaaaaaaaa---------- getView: position = "
+ position +
",convertView = "
+ convertView);
ViewHolder holder;
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{
TextView tv;
CheckBox cb;
}
//分析当前position是否复用了之前哪个位置的view
private
int
analyseReusedWhichPosition(
int
currentPosition){
View currentPositionView = itemViews.get(currentPosition);
for
(
int
i =
0
; i < currentPosition; i++) {
View beforePositionView = 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{42eceab0 V.E..... ......I.
0
,
0
-
0
,
0
}
aaaaaaaaaa---------- getView: position =
1
,convertView =
null
bbbbbbbbbb---------- getView: position =
1
,convertView = android.widget.LinearLayout{42ee4650 V.E..... ......I.
0
,
0
-
0
,
0
}
aaaaaaaaaa---------- getView: position =
2
,convertView =
null
bbbbbbbbbb---------- getView: position =
2
,convertView = android.widget.LinearLayout{42ee6140 V.E..... ......I.
0
,
0
-
0
,
0
}
aaaaaaaaaa---------- getView: position =
3
,convertView =
null
bbbbbbbbbb---------- getView: position =
3
,convertView = android.widget.LinearLayout{42ee7c10 V.E..... ......I.
0
,
0
-
0
,
0
}
aaaaaaaaaa---------- getView: position =
4
,convertView =
null
bbbbbbbbbb---------- getView: position =
4
,convertView = android.widget.LinearLayout{42ee96e0 V.E..... ......I.
0
,
0
-
0
,
0
}
aaaaaaaaaa---------- getView: position =
5
,convertView =
null
bbbbbbbbbb---------- getView: position =
5
,convertView = android.widget.LinearLayout{42eeb1e8 V.E..... ......I.
0
,
0
-
0
,
0
}
aaaaaaaaaa---------- getView: position =
6
,convertView =
null
bbbbbbbbbb---------- getView: position =
6
,convertView = android.widget.LinearLayout{42eeccb8 V.E..... ......I.
0
,
0
-
0
,
0
}
aaaaaaaaaa---------- getView: position =
7
,convertView =
null
bbbbbbbbbb---------- getView: position =
7
,convertView = android.widget.LinearLayout{42eee788 V.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{42ef5150 V.E..... ......I.
0
,
0
-
0
,
0
}
|
3)位置9出现,索引0已经完全消失(复用开始出现)
打印的日志:
1
2
3
4
|
aaaaaaaaaa---------- getView: position =
9
,convertView = android.widget.LinearLayout{42eceab0 V.E..... ........
0
,-
215
-
1080
,
1
}
bbbbbbbbbb---------- getView: position =
9
,convertView = android.widget.LinearLayout{42eceab0 V.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{42ee4650 V.E..... ........
0
,-
213
-
1080
,
3
}
bbbbbbbbbb---------- getView: position =
10
,convertView = android.widget.LinearLayout{42ee4650 V.E..... .......D
0
,-
213
-
1080
,
3
}
getView: 位置
10
复用了位置
1
的view
aaaaaaaaaa---------- getView: position =
11
,convertView = android.widget.LinearLayout{42ee6140 V.E..... ........
0
,-
205
-
1080
,
11
}
bbbbbbbbbb---------- getView: position =
11
,convertView = android.widget.LinearLayout{42ee6140 V.E..... .......D
0
,-
205
-
1080
,
11
}
getView: 位置
11
复用了位置
2
的view
aaaaaaaaaa---------- getView: position =
12
,convertView = android.widget.LinearLayout{42ee7c10 V.E..... ........
0
,-
202
-
1080
,
14
}
bbbbbbbbbb---------- getView: position =
12
,convertView = android.widget.LinearLayout{42ee7c10 V.E..... .......D
0
,-
202
-
1080
,
14
}
getView: 位置
12
复用了位置
3
的view
aaaaaaaaaa---------- getView: position =
13
,convertView = android.widget.LinearLayout{42ee96e0 V.E..... ........
0
,-
201
-
1080
,
15
}
bbbbbbbbbb---------- getView: position =
13
,convertView = android.widget.LinearLayout{42ee96e0 V.E..... .......D
0
,-
201
-
1080
,
15
}
getView: 位置
13
复用了位置
4
的view
aaaaaaaaaa---------- getView: position =
14
,convertView = android.widget.LinearLayout{42eeb1e8 V.E..... ........
0
,-
188
-
1080
,
28
}
bbbbbbbbbb---------- getView: position =
14
,convertView = android.widget.LinearLayout{42eeb1e8 V.E..... .......D
0
,-
188
-
1080
,
28
}
getView: 位置
14
复用了位置
5
的view
aaaaaaaaaa---------- getView: position =
15
,convertView = android.widget.LinearLayout{42eeccb8 V.E..... ........
0
,-
213
-
1080
,
3
}
bbbbbbbbbb---------- getView: position =
15
,convertView = android.widget.LinearLayout{42eeccb8 V.E..... .......D
0
,-
213
-
1080
,
3
}
getView: 位置
15
复用了位置
6
的view
aaaaaaaaaa---------- getView: position =
16
,convertView = android.widget.LinearLayout{42eee788 V.E..... ........
0
,-
179
-
1080
,
37
}
bbbbbbbbbb---------- getView: position =
16
,convertView = android.widget.LinearLayout{42eee788 V.E..... .......D
0
,-
179
-
1080
,
37
}
getView: 位置
16
复用了位置
7
的view
aaaaaaaaaa---------- getView: position =
17
,convertView = android.widget.LinearLayout{42ef5150 V.E..... ........
0
,-
181
-
1080
,
35
}
bbbbbbbbbb---------- getView: position =
17
,convertView = android.widget.LinearLayout{42ef5150 V.E..... .......D
0
,-
181
-
1080
,
35
}
getView: 位置
17
复用了位置
8
的view
aaaaaaaaaa---------- getView: position =
18
,convertView = android.widget.LinearLayout{42eceab0 V.E..... ........
0
,-
195
-
1080
,
21
}
bbbbbbbbbb---------- getView: position =
18
,convertView = android.widget.LinearLayout{42eceab0 V.E..... .......D
0
,-
195
-
1080
,
21
}
getView: 位置
18
复用了位置
0
的view
aaaaaaaaaa---------- getView: position =
19
,convertView = android.widget.LinearLayout{42ee4650 V.E..... ........
0
,-
210
-
1080
,
6
}
bbbbbbbbbb---------- getView: position =
19
,convertView = android.widget.LinearLayout{42ee4650 V.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{42f9a780 V.E..... ........
0
,-
85
-
1080
,
131
}
bbbbbbbbbb---------- getView: position =
8
,convertView = android.widget.LinearLayout{42f9a780 V.E..... .......D
0
,-
85
-
1080
,
131
}
getView: 位置
8
复用了位置
0
的view
aaaaaaaaaa---------- getView: position =
9
,convertView = android.widget.LinearLayout{42f9f818 V.E..... ........
0
,
384
-
1080
,
600
}
bbbbbbbbbb---------- getView: position =
9
,convertView = android.widget.LinearLayout{42f9f818 V.E..... .......D
0
,
384
-
1080
,
600
}
getView: 位置
9
复用了位置
3
的view
aaaaaaaaaa---------- getView: position =
10
,convertView = android.widget.LinearLayout{42f9dd48 V.E..... ........
0
,
138
-
1080
,
354
}
bbbbbbbbbb---------- getView: position =
10
,convertView = android.widget.LinearLayout{42f9dd48 V.E..... .......D
0
,
138
-
1080
,
354
}
getView: 位置
10
复用了位置
2
的view
aaaaaaaaaa---------- getView: position =
11
,convertView = android.widget.LinearLayout{42f9c278 V.E..... ........
0
,-
108
-
1080
,
108
}
bbbbbbbbbb---------- getView: position =
11
,convertView = android.widget.LinearLayout{42f9c278 V.E..... .......D
0
,-
108
-
1080
,
108
}
getView: 位置
11
复用了位置
1
的view
aaaaaaaaaa---------- getView: position =
12
,convertView =
null
bbbbbbbbbb---------- getView: position =
12
,convertView = android.widget.LinearLayout{42fad3a0 V.E..... ......I.
0
,
0
-
0
,
0
}
aaaaaaaaaa---------- getView: position =
13
,convertView = android.widget.LinearLayout{42fa2df0 V.E..... ........
0
,
60
-
1080
,
276
}
bbbbbbbbbb---------- getView: position =
13
,convertView = android.widget.LinearLayout{42fa2df0 V.E..... .......D
0
,
60
-
1080
,
276
}
getView: 位置
13
复用了位置
5
的view
aaaaaaaaaa---------- getView: position =
14
,convertView = android.widget.LinearLayout{42fa12e8 V.E..... ........
0
,-
186
-
1080
,
30
}
bbbbbbbbbb---------- getView: position =
14
,convertView = android.widget.LinearLayout{42fa12e8 V.E..... .......D
0
,-
186
-
1080
,
30
}
getView: 位置
14
复用了位置
4
的view
aaaaaaaaaa---------- getView: position =
15
,convertView = android.widget.LinearLayout{42fa48c0 V.E..... ........
0
,-
150
-
1080
,
66
}
bbbbbbbbbb---------- getView: position =
15
,convertView = android.widget.LinearLayout{42fa48c0 V.E..... .......D
0
,-
150
-
1080
,
66
}
getView: 位置
15
复用了位置
6
的view
aaaaaaaaaa---------- getView: position =
16
,convertView = android.widget.LinearLayout{42f9a780 V.E..... ........
0
,
78
-
1080
,
294
}
bbbbbbbbbb---------- getView: position =
16
,convertView = android.widget.LinearLayout{42f9a780 V.E..... .......D
0
,
78
-
1080
,
294
}
getView: 位置
16
复用了位置
0
的view
aaaaaaaaaa---------- getView: position =
17
,convertView = android.widget.LinearLayout{42f9f818 V.E..... ........
0
,
13
-
1080
,
229
}
bbbbbbbbbb---------- getView: position =
17
,convertView = android.widget.LinearLayout{42f9f818 V.E..... .......D
0
,
13
-
1080
,
229
}
getView: 位置
17
复用了位置
3
的view
aaaaaaaaaa---------- getView: position =
18
,convertView = android.widget.LinearLayout{42f9dd48 V.E..... ........
0
,-
28
-
1080
,
188
}
bbbbbbbbbb---------- getView: position =
18
,convertView = android.widget.LinearLayout{42f9dd48 V.E..... .......D
0
,-
28
-
1080
,
188
}
getView: 位置
18
复用了位置
2
的view
aaaaaaaaaa---------- getView: position =
19
,convertView = android.widget.LinearLayout{42fa6390 V.E..... ........
0
,-
168
-
1080
,
48
}
bbbbbbbbbb---------- getView: position =
19
,convertView = android.widget.LinearLayout{42fa6390 V.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{4304de70 V.E..... ........
0
,-
212
-
1080
,
4
}
bbbbbbbbbb---------- getView: position =
11
,convertView = android.widget.LinearLayout{4304de70 V.E..... .......D
0
,-
212
-
1080
,
4
}
getView: 位置
11
复用了位置
8
的view
aaaaaaaaaa---------- getView: position =
10
,convertView = android.widget.LinearLayout{4303ee70 V.E..... ........
0
,
1829
-
1080
,
2045
}
bbbbbbbbbb---------- getView: position =
10
,convertView = android.widget.LinearLayout{4303ee70 V.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{4303d378 V.E..... ........
0
,
1830
-
1080
,
2046
}
bbbbbbbbbb---------- getView: position =
8
,convertView = android.widget.LinearLayout{4303d378 V.E..... .......D
0
,
1830
-
1080
,
2046
}
getView: 位置
8
复用了位置
0
的view
aaaaaaaaaa---------- getView: position =
7
,convertView = android.widget.LinearLayout{430474b8 V.E..... ........
0
,
1825
-
1080
,
2041
}
bbbbbbbbbb---------- getView: position =
7
,convertView = android.widget.LinearLayout{430474b8 V.E..... .......D
0
,
1825
-
1080
,
2041
}
getView: 位置
7
复用了位置
6
的view
aaaaaaaaaa---------- getView: position =
6
,convertView = android.widget.LinearLayout{43048f88 V.E..... ........
0
,
1824
-
1080
,
2040
}
bbbbbbbbbb---------- getView: position =
6
,convertView = android.widget.LinearLayout{43048f88 V.E..... .......D
0
,
1824
-
1080
,
2040
}
aaaaaaaaaa---------- getView: position =
5
,convertView = android.widget.LinearLayout{43043ee0 V.E..... ........
0
,
1822
-
1080
,
2038
}
bbbbbbbbbb---------- getView: position =
5
,convertView = android.widget.LinearLayout{43043ee0 V.E..... .......D
0
,
1822
-
1080
,
2038
}
getView: 位置
5
复用了位置
4
的view
aaaaaaaaaa---------- getView: position =
4
,convertView = android.widget.LinearLayout{430459e8 V.E..... ........
0
,
1823
-
1080
,
2039
}
bbbbbbbbbb---------- getView: position =
4
,convertView = android.widget.LinearLayout{430459e8 V.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{4304de70 V.E..... ........
0
,
1826
-
1080
,
2042
}
bbbbbbbbbb---------- getView: position =
2
,convertView = android.widget.LinearLayout{4304de70 V.E..... .......D
0
,
1826
-
1080
,
2042
}
aaaaaaaaaa---------- getView: position =
1
,convertView = android.widget.LinearLayout{4303ee70 V.E..... ........
0
,
1828
-
1080
,
2044
}
bbbbbbbbbb---------- getView: position =
1
,convertView = android.widget.LinearLayout{4303ee70 V.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: init checkbox "
+ 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(CompoundButton buttonView,
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上。