Listview优化总结

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 等,这些框架都做了相应处理。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值