ListView 适配器中的 findViewById 与 View Holder 模式

Posted

技术标签:

【中文标题】ListView 适配器中的 findViewById 与 View Holder 模式【英文标题】:findViewById vs View Holder Pattern in ListView adapter 【发布时间】:2013-10-17 20:40:49 【问题描述】:

我总是使用LayoutInflaterfindViewByIdAdaptergetView 方法中创建新项目。

但是在很多文章中人们写到findViewById 非常非常慢,强烈推荐使用 View Holder Pattern。

谁能解释为什么findViewById 这么慢?为什么 View Holder 模式更快?

如果需要向ListView 添加不同的项目,我应该怎么做?我应该为每种类型创建类吗?

static class ViewHolderItem1 
    TextView textViewItem;


static class ViewHolderItem2 
    Button btnViewItem;

static class ViewHolderItem3 
    Button btnViewItem;
    ImageView imgViewItem;

【问题讨论】:

【参考方案1】:

谁能解释为什么 findViewById 这么慢?以及为什么查看持有人 模式更快?

当您不使用 Holder 时,getView() 方法将调用 findViewById() 的次数与您的行不在视图中一样多。因此,如果您在 List 中有 1000 行并且 990 行将不在 View 范围内,那么将再次调用 990 次 findViewById()

Holder 设计模式用于 View 缓存 - Holder(任意)对象保存每一行的子小部件,当行超出 View 时 findViewById() 将不会被调用,但 View 将被回收,小部件将从持有人。

if (convertView == null) 
   convertView = inflater.inflate(layout, null, false);
   holder = new Holder(convertView);
   convertView.setTag(holder); // setting Holder as arbitrary object for row

else  // view recycling
   // row already contains Holder object
   holder = (Holder) convertView.getTag();


// set up row data from holder
titleText.setText(holder.getTitle().getText().toString());

Holder 类可以如下所示:

public class Holder 

   private View row;
   private TextView title;

   public Holder(View row) 
      this.row = row;
   

   public TextView getTitle() 
      if (title == null) 
         title = (TextView) row.findViewById(R.id.title);
      
      return title;
   

正如@meredrica 指出的那样,如果您想获得更好的性能,您可以使用公共字段(但它会破坏封装)。

更新:

这是使用ViewHolder模式的第二种方法:

ViewHolder holder;
// view is creating
if (convertView == null) 
   convertView = LayoutInflater.from(mContext).inflate(R.layout.row, parent, false);
   holder = new ViewHolder();   
   holder.title = (TextView) convertView.findViewById(R.id.title);
   holder.icon = (ImageView) convertView.findViewById(R.id.icon);
   convertView.setTag(holder);

// view is recycling
else 
   holder = (ViewHolder) convertView.getTag();


// set-up row
final MyItem item = mItems.get(position);
holder.title.setText(item.getTitle());
...

private static class ViewHolder 

   public TextView title;
   public ImageView icon;

更新 #2:

众所周知,Google 和 AppCompat v7 作为支持库发布了名为 RecyclerView 的新 ViewGroup,它旨在呈现任何基于适配器的视图。正如 @antonioleiva 在post 中所说:“它应该是 ListView 和 GridView 的继承者”

为了能够使用这个元素,你基本上需要一个最重要的东西,它是一种特殊的适配器,它包含在提到的 ViewGroup 中 - RecyclerView.Adapter 其中 ViewHolder 就是我们正在谈论的东西这里 :) 简单地说,这个新的 ViewGroup 元素实现了自己的 ViewHolder 模式。您需要做的就是创建必须从 RecyclerView.ViewHolder 扩展的自定义 ViewHolder 类,并且您不需要关心检查适配器中的当前行是否为空。

适配器会为你做这件事,你可以确定只有在必须膨胀的情况下才会膨胀该行(我会说)。这是简单的实现:

public static class ViewHolder extends RecyclerView.ViewHolder 

   private TextView title;

   public ViewHolder(View root) 
      super(root);
      title = root.findViewById(R.id.title);
   

这里有两件重要的事情:

您必须调用 super() 构造函数,您需要在其中传递您的 行的根视图 您可以直接从 ViewHolder 获取行的特定位置 通过 getPosition() 方法。当你想做一些事情时,这很有用 在行小部件上点击1 后的操作。

以及适配器中 ViewHolder 的用法。适配器有三个你必须实现的方法:

onCreateViewHolder() - 创建 ViewHolder 的位置 onBindViewHolder() - 更新行的位置。我们可以说是 您正在回收行的一段代码 getItemCount() - 我会说它与典型的 getCount() 方法相同 在 BaseAdapter 中

举个小例子:

@Override 
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) 
   View root = LayoutInflater.from(mContext).inflate(myLayout, parent, false);
   return new ViewHolder(root);


@Override public void onBindViewHolder(ViewHolder holder, int position) 
   Item item = mItems.get(position);
   holder.title.setText(item.getTitle());


@Override public int getItemCount() 
   return mItems != null ? mItems.size() : 0;

1 值得一提的是,RecyclerView 没有提供直接接口来监听项目点击事件。这可能会让某些人感到好奇,但 here is nice explanation 为什么它不像实际看起来那么好奇。

我通过创建自己的界面来解决这个问题,该界面用于处理行上的点击事件(以及您想要的任何类型的小部件):

public interface RecyclerViewCallback<T> 

   public void onItemClick(T item, int position); 

我通过构造函数将其绑定到 Adapter,然后在 ViewHolder 中调用该回调:

root.setOnClickListener(new View.OnClickListener 
   @Override
   public void onClick(View v) 
      int position = getPosition();
      mCallback.onItemClick(mItems.get(position), position);
   
);

这是一个基本的例子,所以不要把它当作一种可能的方式。无限可能。

【讨论】:

另外,findViewById 使用 DOM getter,这很慢。确保不在视图持有者中使用 getter/setter 以获得更高的性能。我知道,这有点破坏封装,但没有人会从一开始就暴露一个视图。 @meredrica 你是对的,但有时公共字段的使用也不错。在这种情况下,使持有者成为内部类并创建公共字段也不错。 我就是这么说的 :) 我永远不会在 Viewholder 之外使用公共可写字段。 感谢您的回答。现在我明白了)但是,我的问题的最后一部分是什么?如果需要向 ListView 添加不同的项目,我应该怎么做?我应该为每种类型创建静态类吗? @Suvtruf 那么在 Cristian 的回答中查看 this thread。【参考方案2】:

ViewHolder 模式将创建 ViewHolder 的静态实例,并在第一次加载时将其附加到视图项,然后在以后的调用中从该视图标记中检索它。我们知道 getView() 方法被非常频繁地调用,特别是当 listview 中有很多元素要滚动时,实际上每次 listview 项目在滚动时可见时都会调用它.

ViewHolder 模式将防止 findViewById() 被多次调用而无用,将视图保持在静态引用上,这是节省一些资源的好模式(特别是当您需要在列表视图项中引用许多视图时)。

@RomainGuy说得很好

ViewHolder 可以也应该用于存储临时数据 在 getView() 中避免内存分配的结构。视图持有者 包含一个 char 缓冲区,以避免在从 光标。

【讨论】:

以上是关于ListView 适配器中的 findViewById 与 View Holder 模式的主要内容,如果未能解决你的问题,请参考以下文章

验证 ListView 适配器中的 EditText

将ListView中的元素与一个适配器中的元素与另一个ListView和适配器中的值匹配

android中的listview计数与适配器计数

带有arraylist的listview,android中的简单适配器

使用自定义适配器更改ListView中特定行中的ImageView

ListView自定义适配器,包括库中的图像