您的位置:首页 > 其它

Listview优化总结

2016-07-28 17:23 309 查看
Listview是andorid中最常用的控件之一,但要用好这个控件并不是那么容易。不注意优化的使用经常出现页面卡顿,OOM等问题的出现。在此本人将自己的拙见整理汇总,归纳listview的优化措施。

1.复用convertView

2.viewHolder保存控件

3.分页加载

4.UI卡顿优化

5.OOM

复用convertView

这一条和下面一条是最常见的优化,相信你也会在各种网络资料或者参考书中见到此类优化。如果还不是很理解为何要这样处理的话,我举一个例子来类比一下这个过程。

相信大家都做过手扶电梯,台阶是由一块块板组成的,运行的过程中,人只需要站在一块板上,就会自动把人送到上方,然后人离开,该块板会在电梯内部重新运行到下方,再次携带一个人上来。

类比到listview,“台阶板”可以看做是子条目item的view,而“人”就是子条目item要显示的数据,屏幕中可见的item就相当于在电梯上的“人”和“板”,当listview往下滑直到某一个item不可见时,就相当于电梯的某块“板”把“人”送到上方直到离开电梯。在不进行优化的情况下,我们在getView方法中每次都会给子条目new一个view出来,当该view不可见后销毁它,把这个过程放到手扶电梯中,就是不断地在下方造“板”,把“人”送到上方后,销毁这块“板”。看到这里相信你肯定在想,这得多浪费资源啊!所以电梯采用的是将“板”在电梯内部重新运行到下方,这样同一块“板”就可以不断地运送“人”。

google工程师在设计listview的时候已经考虑到这种情况,因此在getView的参数中有一个convertView,它是用来保存滑出屏幕范围的item的view,即运送完“人”转入电梯内部的“板”。我们要做的就是获取到这个convertView,重新赋予数据,作为新item来展示,即这块“板”运行到下方,再次运送一个“人”到上方的过程。

电梯将“板”运送到下方需要时间,因此通常电梯“板”的数量是你能看到的2倍。而在内存中一切都是非常快速的,当一个item的view不可见,被保存到convertView后,马上可以作为新的数据的载体,用作下一个item的view来使用,因此,listview仅需要一个屏幕可见item的个数 + 1(convertView)就可以显示所有的数据,无需创建更多的view再销毁,从而达到优化目的。

@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = View.inflate(mContext, android.R.layout.simple_list_item_1, null);
Log.i("test", "inflate a View");
}
TextView textView = (TextView) convertView.findViewById(android.R.id.text1);
textView.setText(data.get(position));
return convertView;
}


通过复用后,测试手机的屏幕可以看到13个item,无论我如何滑动,也只会创建14次view。



viewHolder保存控件

通过上面的代码可以看到,view是不再过多的创建了,但是findViewById这个方法还是在每次getView的时候都会执行,该方法需要遍历整个布局文件来找到对应的ID,是很耗费资源的,有没有什么办法减少这个过程那?

想象一下让你在一本书中找到某一句话,每次你都要从头翻到尾,直到找到那句话,这是非常耗时的。而如果你在第一次找到后,把对应的页数记录下来,下次再让你找的时候,直接翻到对应页面就可以快速地找到那句话。在listview中我们也可以采用同样的方法,将找到的控件记录在内存中,这样下次再找控件的时候直接从内存中取出即可。

通常的做法是定义一个静态内部类ViewHolder,里面仅有成员变量来保存控件:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
convertView = View.inflate(mContext, android.R.layout.simple_list_item_1, null);
holder.textView = (TextView) convertView.findViewById(android.R.id.text1);
convertView.setTag(holder);
Log.i("test", "inflate a View");
}else {
holder = (ViewHolder) convertView.getTag();
}
holder.textView.setText(data.get(position));
return convertView;
}
}
private static class ViewHolder{
TextView textView;
}


是不是会有疑问:为什么要定义成static的那?为什么要叫viewHolder?

非静态内部类拥有外部类的强引用,因此为了避免对外部类(可能是 Activity)对象的引用造成内存泄露,那么最好将内部类声明为 static 的;

其实ViewHolder这个名字可以随意的取,但你写的代码不是只有你自己看,如果你定义为“abc”,那只有你知道这是干嘛的,而其他人不知道,而定义成ViewHolder可以达到见名知意。

分页加载

当listview需要显示的数据较多,数据加载的时间过长,避免用户过长时间的等待数据刷新,可以采用的分页加载的方式对listview进行优化。分页加载的思路就是将大量地数据分成多个小份,每次只加载其中的一份数据,加载新数据的时机即用户滑动到listview底部时。为提升用户体验,在加载数据时在listview底部显示一个progressBar来告诉用户正在加载新数据,并在完成加载后取消这个显示。下面是实现的步骤:

1.inflate一个listview的footer,用于显示progressBar

footer = View.inflate(this, R.layout.footer, null);


2.之前在网上看到的资料都会在setAdapter之前添加footer,setAdater之后移除

//listView.addFooterView(footer);
adapter = new MyAdapter(this, data);
listView.setAdapter(adapter);
//listView.removeFooterView(footer);


此处肯定有疑惑,为啥要如此操作?先来看下setAdapter源码中的一段:

if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}


mFooterViewInfos这个变量一看便知是footer的信息类,当有footer时,传入的adapter会进行包装,因此当时看到的资料才会有如此操作,目的是让adater进行包装。但实际上是多此一举的。继续看下addFooterView方法的源码:

public void addFooterView(View v, Object data, boolean isSelectable) {
final FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mFooterViewInfos.add(info);
mAreAllItemsSelectable &= isSelectable;

// Wrap the adapter if it wasn't already wrapped.
if (mAdapter != null) {
if (!(mAdapter instanceof HeaderViewListAdapter)) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
}

// In the case of re-adding a footer view, or adding one later on,
// we need to notify the observer.
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
}


可以看到内部有判断,如果已经setAdpter,即mAdapter != null条件满足,就会将该adapter进行包装。因此无需做上述操作,仅在需要的时候直接setFooterView就可以了。

3.设置滑动监听,用于判断用户滑动到底部:

listView.setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState==SCROLL_STATE_IDLE){
int lastVisiblePosition = listView.getLastVisiblePosition();
if(lastVisiblePosition==adapter.getCount()-1){
//避免重复加载数据
if(isBottom){
return;
}
isBottom=true;
listView.addFooterView(footer);
listView.setSelection(lastVisiblePosition);
new Thread(new MyTask()).start();
}
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
});


当滑动到底部时添加footer显示progressBar,setSelection调整listview的显示位置,同时开启子线程加载数据。

4.加载数据:

private static class MyTask implements Runnable{
@Override
public void run() {
List<String> newData = new ArrayList<String>();
//模拟数据
count+=20;
//模拟无更多数据加载情况
if(count>150){
handler.sendEmptyMessage(NO_MORE_DATA);
}else{
for (int i = count-20; i < count; i++) {
newData.add("item" + i);
}
//模拟耗时操作
SystemClock.sleep(2000);
//通知主线程更新UI
handler.obtainMessage(RESULT_OK, newData).sendToTarget();
}
}
}


5.更新UI

private static Handler handler = new Handler(){
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case RESULT_OK:
List<String> newData = (List<String>) msg.obj;
data.addAll(newData);
//移除footer
listView.removeFooterView(footer);
//更新数据显示
adapter.notifyDataSetChanged();
isBottom = false;
break;
case NO_MORE_DATA:
View pb = footer.findViewById(R.id.footer_pb);
pb.setVisibility(View.GONE);
View tView = footer.findViewById(R.id.footer_tv);
tView.setVisibility(View.VISIBLE);
isBottom = false;
break;
default:
break;
}
}


NO_MORE_DATA的情况是模拟数据加载完成,更换footer显示内容“无更多数据加载。。。”

UI卡顿优化

1.避免耗时操作在主线程完成,运行所需时间超过16毫秒的任务都会引起页面的卡顿,因此超过16毫秒的任务都应放在子线程完成。

2.UI视图避免过于复杂,过多的层级嵌套,重复的布局可以使用include标签代替,不常用的视图(例如点击才显示)可以使用viewStub标签。可以使用SDK自带的hierarchyviewer工具(androidSDK\tools目录下)查看视图层级。

OOM

如果在listview中显示了大量图片,不进行处理的话,很容易出现OOM的情况。常见的处理方式有:

1.加载图片时对分辨率进行缩放,在android手机中一个像素点会占用4byte的内存,分辨路过大的图片会占用更多的内存。

2.使用弱引用来保存bitmap,并及时调用recycle()方法释放内存。

3.使用已有框架,如ImageLoader、Glide 、Picasso、Fresco 等,这些框架都做了相应处理。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  listview 优化 分页