优化 listview 有哪些方法?

关注者
1,053
被浏览
96,396

21 个回答

为了避免大家误会这个回答只是老生常谈 ListView 的重用机制,编辑一下。

我这里说一下我用 ListView 的一些经验,为了尽量说的全面一些,这里列一些 Tips,具体的代码可以找相关的文章,或者一起交流:

  1. 首先,虽然大家都知道,还是提一下,利用好 convertView 来重用 View,切忌每次 getView() 都新建。ListView 的核心原理就是重用 View。ListView 中有一个回收器,Item 滑出界面的时候 View 会回收到这里,需要显示新的 Item 的时候,就尽量重用回收器里面的 View。
  2. 利用好 View Type,例如你的 ListView 中有几个类型的 Item,需要给每个类型创建不同的 View,这样有利于 ListView 的回收,当然类型不能太多;
  3. 尽量让 ItemView 的 Layout 层次结构简单,这是所有 Layout 都必须遵循的;
  4. 善用自定义 View,自定义 View 可以有效的减小 Layout 的层级,而且对绘制过程可以很好的控制;
  5. 尽量能保证 Adapter 的 hasStableIds() 返回 true,这样在 notifyDataSetChanged() 的时候,如果 id 不变,ListView 将不会重新绘制这个 View,达到优化的目的;
  6. 每个 Item 不能太高,特别是不要超过屏幕的高度,可以参考 Facebook 的优化方法,把特别复杂的 Item 分解成若干小的 Item,特别推荐看一下这个文章:code.facebook.com/posts
  7. 为了保证 ListView 滑动的流畅性,getView() 中要做尽量少的事情,不要有耗时的操作。特别是滑动的时候不要加载图片,停下来再加载,这个库可以帮助你 Glide:github.com/bumptech/gli
  8. 使用 RecycleView 代替。 ListView 每次更新数据都要 notifyDataSetChanged(),有些太暴力了。RecycleView 在性能和可定制性上都有很大的改善,推荐使用。
  9. 有时候,需要从根本上考虑,是否真的要使用 ListView 来实现你的需求,或者是否有其他选择?

写的有点散,有些也是相互穿插的。这里面提到的都是一些原则,详细的解决方案,每个都能在网上找到相关的文章。

有一个小细节,很多开发人员都没有注意过

比如你的Item中有三个按钮,你要为三个按钮分别定义点击事件,如何定义?

也许你会在getView中这样做


button1.setOnclickListener(new View.OnClickListener() {
    @override
    public void onClick(View v) {
        //balabalabala...
    }
});

button2.setOnclickListener(new View.OnClickListener() {
    @override
    public void onClick(View v) {
        //balabalabala...
    }
});

button3.setOnclickListener(new View.OnClickListener() {
    @override
    public void onClick(View v) {
        //balabalabala...
    }
});

如果你每屏显示7个Item,你一共创建了21个listener对象在内存中,如果View回收不畅,会更多,这样,在滚动的时候频繁GC 就会导致卡顿(这里描述有误,请看7月25日更新)

如果你在Adapter初始化的时候创建一个Listener

public MyAdapter () {
    myListener = new View.OnClickListener() {
        @override
        public void onClick(View v) {
            v.getTag()
            v.getId()
            //balabalabala...
        }
    });
}

通过传入的View v这个参数判断是哪一个button被点击,这样,无论View如何创建,你只创建了1个Listener对象

这只是一个小细节,优化的方式要综合使用,才会事半功倍


------------------7月24日更-----------------

v.getTag() 这个tag本不是用来存数据的,通俗点讲它和view 的Id是同一个东西,只不过tag的类型是Object。实际上在tag中存储数据是不符合规范的方式

但其实View类有两种tag,



setTag

(

Object

tag) 方法将tag保存在一个成员变量中,findViewWithTag正是遍历此tag

setTag

(int key,

Object

tag) 方法是最终是调用View类中的setKeyedTag(int key, Object tag)

这是一个私有方法



他是用 SparseArray 实现的,我们可以把需要的东西存到这里面(其实观察源码可以发现,系统很多时候都是这样做的)

这里要注意一点,参数key必须是唯一的,那么我们可以这样做

那么需要先在res/values/strings.xml中添加

<resources>
    <item type="id" name="tag_first"></item>
    <item type="id" name="tag_second"></item>
</resources>

使用的时候写成

view.setTag(R.id.tag_first, obj1);
view.setTag(R.id.tag_second, obj2);

-------------------------关于如何绑定数据的问题-----------------

有人问,如果这样写,所有button只能通过id区分逻辑,无法传入每个item的数据

我们可以将数据通过view 的tag带进来

public View getView(.....) {

....

v.setTag(key, getItem(position));

....

}

然后在listener中通过v.getTag()将数据取出


-----------------------7月25日update-------------------

这里我有一个失误

如果listener里面的逻辑与当前的item有关,那么其实并不只是创建了21个listener对象

public void getView (View convertView ,final int position ....) {
	if (convertView == null) {
		View v = LayoutInflater.from(mContext).inflate(...);
		v.setOnclickListener(new View.OnClickListener () {
			@override
			public void onClick(View v) {
				getItem(position);
			}
		});
	} else {
               
	}
}

你看下,如果绑定数据在convertView为空的情况下确实只创建了有限个listener,

但是在这种情况下绑定上的数据只有View创建时的7个,之后不为空的情况下没有更换listener导致重用的view数据是新的,listener里面的position依然是过去创建view时的7个之一,不会变化(注意参数上的final)

若在else里面再重新setListener,view是有重用,listener被换成新的,并与新的position绑定,老的listener就会变成一个废对象,等待gc回收,随着list滚动,越来越多

关键是我们的业务与position这个参数有关

不知道这回我表述清楚了没有