51

当你在开发一个安卓程序时;并且你想拥有一个ArrayAdapter,你可以简单地拥有一个类(大多数时候使用ViewHolder后缀)或直接膨胀你的convertView并通过 id 找到你的视图。
那么使用 ViewHolder 有什么好处呢?
这里的例子:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = ((Activity)getContext()).getLayoutInflater().inflate(R.layout.row_phrase, null);
    }
    ((TextView) convertView.findViewById(R.id.txtPhrase)).setText("Phrase 01");
}

或者在 ArrayAdapter 中创建一个内部类,如下所示:

static class ViewHolder {   
    ImageView leftIcon;   
    TextView upperLabel;  
    TextView lowerLabel;  
}

最后在 getView 中:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder = null;
    if (view == null) {
        view = LayoutInflater.from(context).inflate(R.layout.row_layout,
                    null, false);
        holder = new ViewHolder();
        holder.leftIcon = (ImageView) view.findViewById(R.id.leftIcon);
    }
}
4

4 回答 4

51

了解列表视图回收的工作原理

ListView 的回收机制是如何工作的

您不能回收当前正在使用的行。上面的链接解释了listview回收机制是如何工作的

那么使用 ViewHolder 有什么好处呢?

引用文档

您的代码可能会findViewById()在 ListView 滚动期间频繁调用,这会降低性能。即使适配器返回一个膨胀的视图进行回收,您仍然需要查找元素并更新它们。避免重复使用的一种方法findViewById()是使用“视图持有者”设计模式。

    public View getView(int position, View convertView, ViewGroup parent) { 
             ViewHolder holder; 

             if (convertView == null) { // if convertView is null
                 convertView = mInflater.inflate(R.layout.mylayout, 
                         parent, false);
                 holder = new ViewHolder(); 
                     // initialize views  
                convertView.setTag(holder);  // set tag on view
            } else { 
                holder = (ViewHolder) convertView.getTag();
                        // if not null get tag 
                        // no need to initialize
            } 

            //update views here  
            return convertView; 
    }

你错过了重要的convertView.setTag(holder)部分holder = (ViewHolder) ConvertView.getTag()

http://developer.android.com/training/improving-layouts/smooth-scrolling.html

于 2014-02-01T17:40:30.480 回答
35

当您浏览您的 ListView 时,在任何给定时间只会显示少数视图。这意味着您不必为适配器中的每个项目都实例化视图;当视图滚动到屏幕外时,它可以被重复使用或回收

视图回收和 ViewHolder 模式不一样。ViewHolder 模式只是为了减少view.findViewById(int)您的调用次数。ViewHolder 模式仅在您利用视图回收时才有效。

getView(int position, View convertView, ViewGroup parent)中,convertView参数要么是null,要么是一个已被回收的视图:它仍然会绑定来自不同列表项的数据。

如果没有 ViewHolder 模式,您仍然可以利用视图回收(即不盲目地实例化视图):

public View getView(int position, View convertView, ViewGroup parent) {
  View view = convertView;
  if (view == null) {
    view = // inflate new view
  }

  ImageView imageView = (ImageView) view.findViewById(R.id.listitem_image);
  TextView textView = (TextView) view.findViewById(R.id.listitem_text);
  TextView timestampView = (TextView) view.findViewById(R.id.listitem_timestamp);
  ProgressBar progressSpinnerView = (ProgressBar) view.findViewById(R.id.progress_spinner);

  // TODO: set correct data for this list item
  // imageView.setImageDrawable(...)
  // textView.setText(...)
  // timestampView.setText(...)
  // progressSpinnerView.setProgress(...)

  return view;
}

上面是一个视图回收的例子——我们不会为每一行膨胀一个新的视图;如果我们没有得到一个可以重用的视图,我们只会夸大一个视图。避免让视图膨胀是在滚动列表时肯定有助于提高性能的部分:利用视图回收。

那么,ViewHolder 有什么用呢?无论行本身是否已经存在,我们目前findViewById(int)每个项目都执行 4 倍。findViewById(int)递归迭代 ViewGroup 直到找到具有给定 ID 的后代时,这对于我们回收的视图来说有点没有意义——我们正在重新查找我们已经引用过的视图。

通过在“找到”子视图后使用 ViewHolder 对象来保存对子视图的引用来避免这种情况:

private static class ViewHolder {
  final TextView text;
  final TextView timestamp;
  final ImageView icon;
  final ProgressBar progress;

  ViewHolder(TextView text, TextView timestamp, ImageView icon, ProgressBar progress) {
    this.text = text;
    this.timestamp = timestamp;
    this.icon = icon;
    this.progress = progress;
  }
}

View.setTag(Object)允许您告诉 View 持有任意对象。如果我们在findViewById(int)调用之后使用它来保存 ViewHolder 的实例,那么我们可以View.getTag()在回收的视图上使用它来避免一次又一次地进行调用。

public View getView(int position, View convertView, ViewGroup parent) {
  View view = convertView;
  if (view == null) {
    view = // inflate new view
    ViewHolder holder = createViewHolderFrom(view);
    view.setTag(holder);  
  }
  ViewHolder holder = view.getTag();
  // TODO: set correct data for this list item
  // holder.icon.setImageDrawable(...)
  // holder.text.setText(...)
  // holder.timestamp.setText(...)
  // holder.progress.setProgress(...)
  return view;
}

private ViewHolder createViewHolderFrom(View view) {
    ImageView icon = (ImageView) view.findViewById(R.id.listitem_image);
    TextView text = (TextView) view.findViewById(R.id.listitem_text);
    TextView timestamp = (TextView) view.findViewById(R.id.listitem_timestamp);
    ProgressBar progress = (ProgressBar) view.findViewById(R.id.progress_spinner);

    return new ViewHolder(text, timestamp, icon, progress);
}

这种优化的性能优势值得怀疑,但这就是ViewHolder 的优势

于 2015-08-16T11:32:37.700 回答
18

ViewHolder设计模式用于加速您的渲染ListView- 实际上是为了使其工作顺利,每次渲染列表项时使用 findViewById 非常昂贵(它进行 DOM 解析),它必须遍历您的布局层次结构并实例化对象。由于列表可以在滚动期间非常频繁地重绘其项目,因此这种开销可能很大。

你可以找到很好的解释它是如何工作的:

http://www.youtube.com/watch?v=wDBM6wVEO70&feature=youtu.be&t=7m

从第 10 分钟开始,您已经解释了 Google 专家的 ViewHolder 设计模式。

[编辑]

findViewById 没有实例化新对象,它只遍历层次结构 - 这里是参考http://androidxref.com/5.1.1_r6/xref/frameworks/base/core/java/android/view/ViewGroup.java#3610

于 2014-02-01T17:49:21.027 回答
3

首先 :

ListView滚动时,ListView您需要创建新项目并将其数据绑定在其上,因此如果其中有很多项目ListView可能会导致内存泄漏,因为您为项目创建了更多对象,但 Android 使用回收其大部分 API 的概念,并且它这意味着您创建一个对象并使用它而不是销毁它并声明一个新对象,因此当您ListView使用滚动的不可见项目滚动 API 并在getView方法中为您传递它时,您可以在convertView这里处理更多项目ListView

第二 :

如果您有自定义项目,则ListView需要在每个项目上附加自定义布局,ListView这样您每次都ListView将绑定新项目findViewById以获取布局项目的参考。此方法将以递归方式搜索您的项目,因此您ViewHolder将帮助您只完成一次递归,然后它将为您保留布局项目的引用,直到您可以附加它ListView

希望这对您有所帮助,并在任何不明显的事情上给我反馈

于 2014-02-01T17:51:36.853 回答