前言
ListView是Android中最常用的控件,通過適配器來進行數據適配然後顯示出來,而其性能是個很值得研究的話題。本文與你一起探討Google I/O提供的優化Adapter方案,歡迎大家交流。
正文
一、准備
1.1 了解關於Google IO大會關於Adapter的優化,參考以下文章:
Android開發之ListView 適配器(Adapter)優化
Android開發——09Google I/O之讓Android UI性能更高效(1)
PDF下載:Google IO.pdf
1.2 准備測試代碼:
Activity
private TestAdapter mAdapter;
private String[] mArrData;
private TextView mTV;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mTV = (TextView) findViewById(R.id.tvShow);
mArrData = new String[1000];
for (int i = 0; i < 1000; i++) {
mArrData[i] = "Google IO Adapter" + i;
}
mAdapter = new TestAdapter(this, mArrData);
((ListView) findViewById(android.R.id.list)).setAdapter(mAdapter);
}
代碼說明:模擬一千條數據,TestAdapter繼承自BaseAdapter,main.xml見文章末尾下載。
二、測試
測試方法:手動滑動ListView至position至50然後往回滑動,充分利用convertView不等於null的代碼段。
2.1 方案一
按照Google I/O介紹的第二種方案,把item子元素分別改為4個和10個,這樣效果更佳明顯。
2.1.1 測試代碼
private int count = 0;
private long sum = 0L;
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//開始計時
long startTime = System.nanoTime();
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item_icon_text,
null);
}
((ImageView) convertView.findViewById(R.id.icon1)).setImageResource(R.drawable.icon);
((TextView) convertView.findViewById(R.id.text1)).setText(mData[position]);
((ImageView) convertView.findViewById(R.id.icon2)).setImageResource(R.drawable.icon);
((TextView) convertView.findViewById(R.id.text2)).setText(mData[position]);
//停止計時
long endTime = System.nanoTime();
//計算耗時
long val = (endTime - startTime) / 1000L;
Log.e("Test", "Position:" + position + ":" + val);
if (count < 100) {
if (val < 1000L) {
sum += val;
count++;
}
} else
mTV.setText(String.valueOf(sum / 100L));//顯示統計結果
return convertView;
}
2.1.2 測試結果(微秒除以1000,見代碼)
次數
4個子元素
10個子元素
第一次
366
723
第二次
356
689
第三次
371
692
第四次
356
696
第五次
371
662
2.2 方案二
按照Google I/O介紹的第三種方案,是把item子元素分別改為4個和10個。
2.2.1 測試代碼
private int count = 0;
private long sum = 0L;
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 開始計時
long startTime = System.nanoTime();
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item_icon_text,
null);
holder = new ViewHolder();
holder.icon1 = (ImageView) convertView.findViewById(R.id.icon1);
holder.text1 = (TextView) convertView.findViewById(R.id.text1);
holder.icon2 = (ImageView) convertView.findViewById(R.id.icon2);
holder.text2 = (TextView) convertView.findViewById(R.id.text2);
convertView.setTag(holder);
}
else{
holder = (ViewHolder)convertView.getTag();
}
holder.icon1.setImageResource(R.drawable.icon);
holder.text1.setText(mData[position]);
holder.icon2 .setImageResource(R.drawable.icon);
holder.text2.setText(mData[position]);
// 停止計時
long endTime = System.nanoTime();
// 計算耗時
long val = (endTime - startTime) / 1000L;
Log.e("Test", "Position:" + position + ":" + val);
if (count < 100) {
if (val < 1000L) {
sum += val;
count++;
}
} else
mTV.setText(String.valueOf(sum / 100L));// 顯示統計結果
return convertView;
}
}
static class ViewHolder {
TextView text1;
ImageView icon1;
TextView text2;
ImageView icon2;
}
2.2.2 測試結果(微秒除以1000,見代碼)
次數
4個子元素
10個子元素
第一次
311
417
第二次
291
441
第三次
302
462
第四次
286
444
第五次
299
436
2.3
方案三
此方案為“Henry Hu”提示,API Level 4以上提供,這裡順帶測試了一下不使用靜態內部類情況下性能。
2.3.1 測試代碼
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 開始計時
long startTime = System.nanoTime();
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item_icon_text, null);
convertView.setTag(R.id.icon1, convertView.findViewById(R.id.icon1));
convertView.setTag(R.id.text1, convertView.findViewById(R.id.text1));
convertView.setTag(R.id.icon2, convertView.findViewById(R.id.icon2));
convertView.setTag(R.id.text2, convertView.findViewById(R.id.text2));
}
((ImageView) convertView.getTag(R.id.icon1)).setImageResource(R.drawable.icon);
((ImageView) convertView.getTag(R.id.icon2)).setImageResource(R.drawable.icon);
((TextView) convertView.getTag(R.id.text1)).setText(mData[position]);
((TextView) convertView.getTag(R.id.text2)).setText(mData[position]);
// 停止計時
long endTime = System.nanoTime();
// 計算耗時
long val = (endTime - startTime) / 1000L;
Log.e("Test", "Position:" + position + ":" + val);
if (count < 100) {
if (val < 1000L) {
sum += val;
count++;
}
} else
mTV.setText(String.valueOf(sum / 100L) + ":" + nullcount);// 顯示統計結果
return convertView;
}
2.3.2 測試結果(微秒除以1000,見代碼)
第一次:450
第二次:467
第三次:472
第四次:451
第五次:441
四、總結
4.1 首先有一個認識是錯誤的,我們先來看截圖:
可以發現,只有第一屏(可視范圍)調用getView所消耗的時間遠遠多於後面的,通過對
convertView == null內代碼監控也是同樣的結果。
也就是說ListView僅僅緩存了可視范圍內的View,隨後的滾動都是對這些View進行數據更新。不管你有多少數據,他都只用ArrayList緩存可視范圍內的View,這樣保證了性能,也造成了我以為ListView只緩存View結構不緩存數據的假相(不會只有我一人這麼認為吧- - #)。這也能解釋為什麼GOOGLE優化方案一比二高很多的原因。那麼剩下的也就只有findViewById比較耗時了。據此大家可以看看AbsListView的源代碼,看看
obtainView這個方法內的代碼及RecycleBin這個類的實現,歡迎分享。
此外了解這個原理了,那麼以下代碼不運行你可能猜到結果了:
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item_icon_text, null);
((ImageView) convertView.findViewById(R.id.icon1)).setImageResource(R.drawable.icon);
((TextView) convertView.findViewById(R.id.text1)).setText(mData[position]);
((ImageView) convertView.findViewById(R.id.icon2)).setImageResource(R.drawable.icon);
((TextView) convertView.findViewById(R.id.text2)).setText(mData[position]);
}
else
return convertView;
沒錯,你會發現滾動時會重復顯示第一屏的數據!
子控件裡的事件因為是同一個控件,也可以直接放到convertView == null 代碼塊內部,如果需要交互數據比如position,可以通過tag方式來設置並獲取當前數據。
4.2 本文方案一與方案二對比
這裡推薦如果只是一般的應用(一般指子控件不多),無需都是用靜態內部類來優化,使用第二種方案即可;反之,對性能要求較高時可采用。此外需要提醒的是這裡也是用空間換時間的做法,View本身因為setTag而會占用更多的內存,還會增加代碼量;而findViewById會臨時消耗更多的內存,所以不可盲目使用,依實際情況而定。
4.3 方案三
此方案為“Henry Hu”提示,API Level 4以上支持,原理和方案三一致,減少findViewById次數,但是從測試結果來看效果並不理想,這裡不再做進一步的測試。
五、推薦文章
Android,誰動了我的內存(1)
Android 內存洩漏調試
結束
對於Google I/O大會這個優化方案一直抱遲疑態度,此番測試總算是有了更進一步的了解,歡迎大家先測試後交流,看看還有什麼辦法能夠再優化一點。