一步一步實現listview加載的性能優化
來自: http://www.cnblogs.com/goagent/p/5158064.html
listview加載的核心是其adapter,本文針對listview加載的性能優化就是對adpter的優化,總共分四個層次:
0、最原始的加載
1、利用convertView
2、利用ViewHolder
3、實現局部刷新
[轉載請保留本文地址:http://www.cnblogs.com/goagent/p/5158064.html]
〇、最原始的加載
這里是不經任何優化的adapter,為了看起來方便,把listview的數據直接在構造函數里傳給adapter了,代碼如下:
1 private class AdapterOptmL0 extends BaseAdapter {
2 private LayoutInflater mLayoutInflater;
3 private ArrayList<Integer> mListData;
4
5 public AdapterOptmL0(Context context, ArrayList<Integer> data) {
6 mLayoutInflater = LayoutInflater.from(context);
7 mListData = data;
8 }
9
10 @Override
11 public int getCount() {
12 return mListData == null ? 0 : mListData.size();
13 }
14
15 @Override
16 public Object getItem(int position) {
17 return mListData == null ? 0 : mListData.get(position);
18 }
19
20 @Override
21 public long getItemId(int position) {
22 return position;
23 }
24
25 @Override
26 public View getView(int position, View convertView, ViewGroup parent) {
27 View viewRoot = mLayoutInflater.inflate(R.layout.listitem, parent, false);
28 if (viewRoot != null) {
29 TextView txt = (TextView)viewRoot.findViewById(R.id.listitem_txt);
30 txt.setText(getItem(position) + "");
31 }
32 return viewRoot;
33 }
34 }
[轉載請保留本文地址:http://www.cnblogs.com/goagent/p/5158064.html]
一、利用 convertView
上述代碼的第27行在Eclipse中已經提示警告:
Unconditional layout inflation from view adapter: Should use View Holder pattern (use recycled view passed into this method as the second parameter) for smoother scrolling
這個意思就是說,被移出可視區域的view是可以回收復用的,它作為getview的第二個參數已經傳進來了,所以沒必要每次都從xml里inflate。
經過優化后的代碼如下:
1 @Override
2 public View getView(int position, View convertView, ViewGroup parent) {
3 if (convertView == null) {
4 convertView = mLayoutInflater.inflate(R.layout.listitem, parent, false);
5 }
6 if (convertView != null) {
7 TextView txt = (TextView)convertView.findViewById(R.id.listitem_txt);
8 txt.setVisibility(View.VISIBLE);
9 txt.setText(getItem(position) + "");
10 }
11 return convertView;
12 }
上述代碼加了判斷,如果傳入的convertView不為null,則直接復用,否則才會從xml里inflate。
按照上述代碼,如果手機一屏最多同時顯示5個listitem,則最多需要從xml里inflate 5 次,比AdapterOptmL0中每個listitem都需要inflate顯然效率高多了。
上述的用法雖然提高了效率,但帶來了一個 陷阱 , 如果復用convertView,則需要重置該view所有可能被修改過的屬性 。
舉個例子:
如果第一個view中的textview在getview中被設置成INVISIBLE了,而現在第一個view在滾動過程中出可視區域,并假設它作為參數傳入第十個view的getview而被復用
那么,在第十個view的getview里面不僅要setText,還要重新setVisibility,因為這個被復用的view當前處于INVISIBLE狀態!
[轉載請保留本文地址:http://www.cnblogs.com/goagent/p/5158064.html]
二、利用ViewHolder
從AdapterOptmL0第27行的警告中,我們還可以看到編譯器推薦了一種模型叫ViewHolder,這是個什么東西呢,先看代碼:
1 private class AdapterOptmL2 extends BaseAdapter {
2 private LayoutInflater mLayoutInflater;
3 private ArrayList<Integer> mListData;
4
5 public AdapterOptmL2(Context context, ArrayList<Integer> data) {
6 mLayoutInflater = LayoutInflater.from(context);
7 mListData = data;
8 }
9
10 private class ViewHolder {
11 public ViewHolder(View viewRoot) {
12 txt = (TextView)viewRoot.findViewById(R.id.listitem_txt);
13 }
14 public TextView txt;
15 }
16
17 @Override
18 public int getCount() {
19 return mListData == null ? 0 : mListData.size();
20 }
21
22 @Override
23 public Object getItem(int position) {
24 return mListData == null ? 0 : mListData.get(position);
25 }
26
27 @Override
28 public long getItemId(int position) {
29 return position;
30 }
31
32 @Override
33 public View getView(int position, View convertView, ViewGroup parent) {
34 if (convertView == null) {
35 convertView = mLayoutInflater.inflate(R.layout.listitem, parent, false);
36 ViewHolder holder = new ViewHolder(convertView);
37 convertView.setTag(holder);
38 }
39 if (convertView != null && convertView.getTag() instanceof ViewHolder) {
40 ViewHolder holder = (ViewHolder)convertView.getTag();
41 holder.txt.setVisibility(View.VISIBLE);
42 holder.txt.setText(getItem(position) + "");
43 }
44 return convertView;
45 }
46 }
從代碼中可以看到,這一步做的優化是用一個類ViewHolder來保存listitem里面所有找到的子控件,這樣就不用每次都通過耗時的findViewById操作了。
這一步的優化,在listitem布局越復雜的時候效果越為明顯。
[轉載請保留本文地址:http://www.cnblogs.com/goagent/p/5158064.html]
三、實現局部刷新
OK,到目前為止,listview普遍需要的優化已經做的差不多了,那就該考慮實際使用場景中的優化需求了。
實際使用listview過程中,通常會在后臺更新listview的數據,然后調用Adatper的notifyDataSetChanged方法來更新listview的UI。
那么問題來了,一般情況下,一次只會更新listview的一條/幾條數據,而調用notifyDataSetChanged方法則會把所有可視范圍內的listitem都刷新一遍,這是不科學的!
所以,進一步優化的空間在于,局部刷新listview,話不多說見代碼:
private class AdapterOptmL3 extends BaseAdapter {
private LayoutInflater mLayoutInflater;
private ListView mListView;
private ArrayList<Integer> mListData;
public AdapterOptmL3(Context context, ListView listview, ArrayList<Integer> data) {
mLayoutInflater = LayoutInflater.from(context);
mListView = listview;
mListData = data;
}
private class ViewHolder {
public ViewHolder(View viewRoot) {
txt = (TextView)viewRoot.findViewById(R.id.listitem_txt);
}
public TextView txt;
}
@Override
public int getCount() {
return mListData == null ? 0 : mListData.size();
}
@Override
public Object getItem(int position) {
return mListData == null ? 0 : mListData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mLayoutInflater.inflate(R.layout.listitem, parent, false);
ViewHolder holder = new ViewHolder(convertView);
convertView.setTag(holder);
}
if (convertView != null && convertView.getTag() instanceof ViewHolder) {
updateView((ViewHolder)convertView.getTag(), (Integer)getItem(position));
}
return convertView;
}
public void updateView(ViewHolder holder, Integer data) {
if (holder != null && data != null) {
holder.txt.setVisibility(View.VISIBLE);
holder.txt.setText(data + "");
}
}
public void notifyDataSetChanged(int position) {
final int firstVisiablePosition = mListView.getFirstVisiblePosition();
final int lastVisiablePosition = mListView.getLastVisiblePosition();
final int relativePosition = position - firstVisiablePosition;
if (position >= firstVisiablePosition && position <= lastVisiablePosition) {
updateView((ViewHolder)mListView.getChildAt(relativePosition).getTag(), (Integer)getItem(position));
} else {
//不在可視范圍內的listitem不需要手動刷新,等其可見時會通過getView自動刷新
}
}
}</pre>
修改后的Adapter新增了一個方法 public void notifyDataSetChanged( int position) 可以根據position只更新指定的listitem。
[轉載請保留本文地址:http://www.cnblogs.com/goagent/p/5158064.html]
局部刷新番外篇
在局部刷新數據的接口中,實際上還可以再干點事情:listview正在滾動的時候不去刷新。
具體的思路是,如果當前正在滾動,則記住一個pending任務,等listview停止滾動的時候再去刷,這樣不會造成滾動的時候刷新錯亂。代碼如下:
1 private class AdapterOptmL3Plus extends BaseAdapter implements OnScrollListener{
2 private LayoutInflater mLayoutInflater;
3 private ListView mListView;
4 private ArrayList<Integer> mListData;
5
6 private int mScrollState = SCROLL_STATE_IDLE;
7 private List<Runnable> mPendingNotify = new ArrayList<Runnable>();
8
9 public AdapterOptmL3Plus(Context context, ListView listview, ArrayList<Integer> data) {
10 mLayoutInflater = LayoutInflater.from(context);
11 mListView = listview;
12 mListData = data;
13 mListView.setOnScrollListener(this);
14 }
15
16 private class ViewHolder {
17 public ViewHolder(View viewRoot) {
18 txt = (TextView)viewRoot.findViewById(R.id.listitem_txt);
19 }
20 public TextView txt;
21 }
22
23 @Override
24 public int getCount() {
25 return mListData == null ? 0 : mListData.size();
26 }
27
28 @Override
29 public Object getItem(int position) {
30 return mListData == null ? 0 : mListData.get(position);
31 }
32
33 @Override
34 public long getItemId(int position) {
35 return position;
36 }
37
38 @Override
39 public View getView(int position, View convertView, ViewGroup parent) {
40 if (convertView == null) {
41 convertView = mLayoutInflater.inflate(R.layout.listitem, parent, false);
42 ViewHolder holder = new ViewHolder(convertView);
43 convertView.setTag(holder);
44 }
45 if (convertView != null && convertView.getTag() instanceof ViewHolder) {
46 updateView((ViewHolder)convertView.getTag(), (Integer)getItem(position));
47 }
48 return convertView;
49 }
50
51 public void updateView(ViewHolder holder, Integer data) {
52 if (holder != null && data != null) {
53 holder.txt.setVisibility(View.VISIBLE);
54 holder.txt.setText(data + "");
55 }
56 }
57
58 public void notifyDataSetChanged(final int position) {
59 final Runnable runnable = new Runnable() {
60 @Override
61 public void run() {
62 final int firstVisiablePosition = mListView.getFirstVisiblePosition();
63 final int lastVisiablePosition = mListView.getLastVisiblePosition();
64 final int relativePosition = position - firstVisiablePosition;
65 if (position >= firstVisiablePosition && position <= lastVisiablePosition) {
66 if (mScrollState == SCROLL_STATE_IDLE) {
67 //當前不在滾動,立刻刷新
68 Log.d("Snser", "notifyDataSetChanged position=" + position + " update now");
69 updateView((ViewHolder)mListView.getChildAt(relativePosition).getTag(), (Integer)getItem(position));
70 } else {
71 synchronized (mPendingNotify) {
72 //當前正在滾動,等滾動停止再刷新
73 Log.d("Snser", "notifyDataSetChanged position=" + position + " update pending");
74 mPendingNotify.add(this);
75 }
76 }
77 } else {
78 //不在可視范圍內的listitem不需要手動刷新,等其可見時會通過getView自動刷新
79 Log.d("Snser", "notifyDataSetChanged position=" + position + " update skip");
80 }
81 }
82 };
83 runnable.run();
84 }
85
86 @Override
87 public void onScrollStateChanged(AbsListView view, int scrollState) {
88 mScrollState = scrollState;
89 if (mScrollState == SCROLL_STATE_IDLE) {
90 //滾動已停止,把需要刷新的listitem都刷新一下
91 synchronized (mPendingNotify) {
92 final Iterator<Runnable> iter = mPendingNotify.iterator();
93 while (iter.hasNext()) {
94 iter.next().run();
95 iter.remove();
96 }
97 }
98 }
99 }
100
101 @Override
102 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
103 }
104 }
View Code</pre> </div>