ListView优化(三) 【来自知乎上的汇总,很有见地!!】

来源:互联网 发布:淘宝手机助手4,9 编辑:程序博客网 时间:2024/04/30 02:08

总结一下:

  • 复用convertView对象,减小内存压力
  • 使用ViewHolder,减少findViewById次数,在VIewHolder 使用关键字 static 
  • 使用图片缓存、压缩图片、异步加载图片
  • listview滑动的时候,不加载图片,让滑动更加流畅
  • 只加载可见item的图片
/********************************************** 华丽的分割线 ******************************************************/

根据楼主的简单描述,“优化”一词简单认定为性能优化吧,以FPS为指标,可以从这么几点去考虑:
1. 界面是否过渡绘制
2. 布局是否合理
3. 对象是否循环可利用,即最少的new操作
4. UI线程是否不被阻塞 io与ui分配,甚至new操作与ui线程分离

/********************************************** 华丽的分割线 ******************************************************/

充分利用已生成的View对象,减少系统生成的view个数,并充分利用view对象的TAG,避免系统大量生成一次性使用的数据。
绘制ListView之前,将会先调用getView方法来获取Item的个数。之后每绘制一个 Item就会调用一次getView方法,可以引用事先定义好的xml来确定显示的效果并返回一个View对象作为一个Item显示出来。也正是在这个过程中完成了适配器的主要转换功能,把数据以开发者想要的效果显示出来。通过getView的重复调用,可以使得ListView的使用更为简单和灵活。

/********************************************** 华丽的分割线 ******************************************************/

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

比如你的Item中有三个按钮,你要为三个按钮分别定义点击事件,如何定义?
也许你会在getView中这样做button1.setOnclickListener(newView.OnClickListener(){
如果你每屏显示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对象
这只是一个小细节,优化的方式要综合使用,才会事半功倍
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()将数据取出
这里我有一个失误
如果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 () {}
你看下,如果绑定数据在convertView为空的情况下确实只创建了有限个listener,
但是在这种情况下绑定上的数据只有View创建时的7个,之后不为空的情况下没有更换listener导致重用的view数据是新的,listener里面的position依然是过去创建view时的7个之一,不会变化(注意参数上的final)

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

/********************************************** 华丽的分割线 ******************************************************/

为了避免大家误会这个回答只是老生常谈 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 来实现你的需求,或者是否有其他选择?
/********************************************** 华丽的分割线 ******************************************************/

ListView如何运作的?

ListView是设计应用于对可扩展性和高性能要求的地方。实际上,这就意味着ListView有以下2个要求:

  1. 尽可能少的创建View;
  2. 只是绘制和布局在屏幕上可见的子View。

理解第一点很简单:通过布局xml文件在创建View并显示是很昂贵耗时耗资源的操作。尽管布局文件已经编译打包成了二进制形式以便于更高效的语法解析,但是创建View仍然需要通过一个特殊的XML树,并实例化所有需要响应的View。

ListView通过回收一些不可见的Views,通常在Android源码中称为“ScrapView(废弃的View)”来解决这个问题。这及意味着开发者只需要简单的更新每行的内容而不需要针对每个单独的行的布局来创建View。

为了实现第二点,在我们滑动屏幕时,ListView通过使用View回收器来增加低于或者高于当当前窗口的Views,并当前活动的Views移动到一个可回收池中。这样的话,ListView只需要在内存中保持足够多的Views去填充分配空间中的布局和一些额外的可回收Views,即使当你的Adapter有上百个items的适合。它会使用不同的方法去填充行之间的空间,从顶部或者底部等等,具体取决于窗口是如何变化的。

下面这个图很直观的展示了当你按下ListView的时候发生了什么:

ListView

通过上述介绍,相比我们已经熟悉了ListView的这种机制,让我们继续前往技巧部分。正如上述介绍的,在滑动时,ListView通过动态的创建和回收很多View,实现了尽可能地让Adapter的getView()轻量。所有的技巧都是通过多种方法让getView()更快。

## View的回收

ListView每次需要在屏幕上显示新的一行的时候,会从其Adapter中调用getView()的方法。众所周知,getView()方法有3个参数:行的位置, convertView以及父ViewGroup。

参数convertView说穿来就是之前讲述的ScrapView。当ListView要求更新一行的布局时,convertView是一个非空值。因此,当convertView值非空时,你仅仅需要更新内容即可,而不需要重新一个新行的布局。getView()在Adapter中一般是如下的形式:

public View getView(int position, View convertView, ViewGroup parent) {    if (convertView == null) {        convertView = mInflater.inflate(R.layout.your_layout, null);    }    TextView text = (TextView) convertView.findViewById(R.id.text);    text.setText("Position " + position);    return convertView;}

## View Holder如何写的模板

Android很常见的一个操作就是在布局文件中找到一个内部的View。通常是使用一个findViewById()的View方法来实现的。这个findViewById()方法在View树中,根据一个View ID,会递归的被调用来找到其子树。虽然在静态UI布局中使用findViewById()是完全正常的。但是,在滑动时,ListView调用其Adapter中的getView()是非常频繁的。findViewById()可能会影响ListView滑动时的性能,尤其是你的行布局是很复杂的时候。

寻找一个充气布局内的内部观点是在Android上最常用的操作之一。这通常是通过一个名为findViewById(查看方法完成)。此方法将递归经过视图树寻找一个孩子用给定的ID码。静态的UI布局使用findViewById()是完全正常,但正如你所看到的,ListView中滚动时调用适配器的getView()非常频繁。 findViewById()可能perceivably击中ListViews,尤其是滚动的性能,如果你行的布局是不平凡的。

View Holder的模式就是减少在Adapter中getView()方法中调用findViewById()次数。实际上,View Holder是一个轻量级的内部类,用于直接引用到所有内部views。在创建View之后,你可以在每行的View存储为一个标签。通过这种方法,只需要在初次创建布局的时候调用findViewById()。下面是一个使用上述方法的View Holder模板的代码示例:

public View getView(int position, View convertView, ViewGroup parent) {    ViewHolder holder;    if (convertView == null) {        convertView = mInflater.inflate(R.layout.your_layout, null);        holder = new ViewHolder();        holder.text = (TextView) convertView.findViewById(R.id.text);        convertView.setTag(holder);    } else {        holder = convertView.getTag();    }    holder.text.setText("Position " + position);    return convertView;}private static class ViewHolder {    public TextView text;}

## 异步加载

很多时候,Android应用在ListView每行中显示一些多媒体内容,比如图片等。在Adapter中的getView()使用应用内置的图片资源还是不会出什么问题的,因为可以存储在Android的高速缓存中。但当你想多态的显示来自本地磁盘或网络的内容时,例如缩略图,简历图片等。在这种情况下,你可能不希望直接在Adapter中的getView()加载它们,因为IO进程会阻塞UI线程。如果这样做的话,ListView就看起来非常卡顿。

在一个单独的线程,如果想要运行的所有行的IO操作或任何高负载CPU限制的异步操作。其中的技巧就是要做到符合ListView的回收行为。例如,如果在Adapter中的getView()中,使用AsyncTask的加载去加载资料图片,在AsyncTask完成之前,你正在加载的图片View就有可能被回收用于其他地方。所以,一旦异步操作完成的同时,需要一种机制来知道如果相应的View有没有被回收,。

一个简单的方法来实现这一目标是通过附加一些标识该行与它相关的View的信息。然后,当异步操作完成的适合,检查目标行的View和标识的View是否一致。实现这一目标的方法很多。下面是实现这种方法的一个很简单的示例:

public View getView(int position, View convertView,        ViewGroup parent) {    ViewHolder holder;    ...    holder.position = position;    new ThumbnailTask(position, holder)            .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);    return convertView;}private static class ThumbnailTask extends AsyncTask {    private int mPosition;    private ViewHolder mHolder;    public ThumbnailTask(int position, ViewHolder holder) {        mPosition = position;        mHolder = holder;    }    @Override    protected Cursor doInBackground(Void... arg0) {        // Download bitmap here    }    @Override    protected void onPostExecute(Bitmap bitmap) {        if (mHolder.position == mPosition) {            mHolder.thumbnail.setImageBitmap(bitmap);        }    }}private static class ViewHolder {    public ImageView thumbnail;    public int position;}

## 人机交互知识

做到在每一行异步加载很多资源,是一个高性能的ListView的必经之路。但是,在滑动屏幕时,如果你一味的在每一个getView()调用里面都去启动一个异步的操作,造成的结果就是你会浪费大量资源。因为行被频繁回收,造成大部分返回的结果会被丢弃。

考虑到实际的人机交互情况,在ListView适配器中,在每一行中都不应该去触发任何异步操作。也就是说,在ListView中有fling(快速滑动)操作时,启动任何异步操作都没有任何意义。一旦滚动停止或即将停止,才是开始真正显示每行的内容的时候。

我不会发布一个代码示例贴在这里,因为其中涉及到的代码太多。Romain Guy写了一个很经典的应用:Shelves app,其中有一个很好的的示例。当GridView停止滑动时不做其他事情时,它就开始触发从而去异步加载书的封面资源。即使在滑动时,你也可以展示缓存中的内容,通过使用memory cache来平衡交互。这真是个好主意!


0 0
原创粉丝点击