ListView出现内存不足异常,但没有内存泄漏?

3
在Honeycomb之后,谷歌表示位图由堆管理(在这里讨论),所以如果一个位图不再可访问,我们可以假设GC会处理它并释放它。
我想创建一个演示程序,展示listView讲座中提到的思路的效率(从这里),所以我制作了一个小应用程序。该应用程序允许用户按下按钮,然后listview将滚动到底部,同时具有10000个项目,它们的内容是android.R.drawable项目(名称和图像)。
由于某种原因,即使我没有保存任何图像,我也会出现内存不足的情况,所以我的问题是:怎么会呢?我错过了什么?
我已经在Galaxy S III上测试了该应用程序,但如果使用适配器的本机版本,我仍然会出现内存不足异常。我不明白为什么会发生这种情况,因为我没有存储任何东西。
以下是代码:
public class MainActivity extends Activity
  {
  private static final int LISTVIEW_ITEMS =10000;
  long                     _startTime;
  boolean                  _isMeasuring   =false;

  @Override
  public void onCreate(final Bundle savedInstanceState)
    {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final ListView listView=(ListView)findViewById(R.id.listView);
    final Field[] fields=android.R.drawable.class.getFields();
    final LayoutInflater inflater=(LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    // listen to scroll events , so that we publish the time only when scrolled to the bottom:
    listView.setOnScrollListener(new OnScrollListener()
      {
        @Override
        public void onScrollStateChanged(final AbsListView view,final int scrollState)
          {
          if(!_isMeasuring||view.getLastVisiblePosition()!=view.getCount()-1||scrollState!=OnScrollListener.SCROLL_STATE_IDLE)
            return;
          final long stopTime=System.currentTimeMillis();
          final long scrollingTime=stopTime-_startTime;
          Toast.makeText(MainActivity.this,"time taken to scroll to bottom:"+scrollingTime,Toast.LENGTH_SHORT).show();
          _isMeasuring=false;
          }

        @Override
        public void onScroll(final AbsListView view,final int firstVisibleItem,final int visibleItemCount,final int totalItemCount)
          {}
      });
    // button click handling (start measuring) :
    findViewById(R.id.button).setOnClickListener(new OnClickListener()
      {
        @Override
        public void onClick(final View v)
          {
          if(_isMeasuring)
            return;
          final int itemsCount=listView.getAdapter().getCount();
          listView.smoothScrollToPositionFromTop(itemsCount-1,0,1000);
          _startTime=System.currentTimeMillis();
          _isMeasuring=true;
          }
      });
    // creating the adapter of the listView
    listView.setAdapter(new BaseAdapter()
      {
        @Override
        public View getView(final int position,final View convertView,final ViewGroup parent)
          {
          final Field field=fields[position%fields.length];
          // final View inflatedView=convertView!=null ? convertView : inflater.inflate(R.layout.list_item,null);
          final View inflatedView=inflater.inflate(R.layout.list_item,null);
          final ImageView imageView=(ImageView)inflatedView.findViewById(R.id.imageView);
          final TextView textView=(TextView)inflatedView.findViewById(R.id.textView);
          textView.setText(field.getName());
          try
            {
            final int imageResId=field.getInt(null);
            imageView.setImageResource(imageResId);
            }
          catch(final Exception e)
            {}
          return inflatedView;
          }

        @Override
        public long getItemId(final int position)
          {
          return 0;
          }

        @Override
        public Object getItem(final int position)
          {
          return null;
          }

        @Override
        public int getCount()
          {
          return LISTVIEW_ITEMS;
          }
      });
    }
  }

@all:我知道针对这段代码有优化的方法(使用convertView和viewHolder设计模式),因为我已经提到了Google制作的listView视频。相信我,我知道什么更好;这就是代码的整个重点。

上面的代码应该表明使用您(和视频)展示的东西更好。但是首先我需要展示幼稚的方式;即使是幼稚的方式也应该能够工作,因为我不存储位图或视图,并且因为Google进行了相同的测试(因此他们得到了性能比较图)。


我已经知道了。然而,正如你所看到的,我没有存储任何位图,它们非常小并且由系统使用。 - android developer
1
糟糕,忽略前面的内容。你的问题在于你没有使用传递给getView()方法的convertView,因此你正在尝试创建10000个视图对象而不是只需足够填充屏幕的数量。如果那是我记得看过的相同的ListView课程,Romain肯定会在那里谈论如何使用convertView。 - FoamyGuy
关于“因为我不存储位图或视图”,在下面的答案中,我解释了为什么内存不足不是由位图分配引起的(您可以通过注释掉我提到的代码部分来进行测试),而是由于ListView积累的大量视图,因为您忽略了convertView参数。 - Joe
但是根据他们的视频,谷歌已经进行了相同的测试。他们如何在没有出现这个问题的情况下使用了10000个项目? - android developer
@androiddeveloper 你所遇到的问题正是你所做的“天真”方式的原因。事实上,它对你不起作用,而对他们起作用(如果确实如此,我不记得具体情况),可能更多地与特定设备有关,也许他们的设备有足够的内存来跟上,而你的设备没有。无论如何,整个练习的重点是要看到忽略convertView是不好的,这应该是相当明显的,因为它会导致应用程序崩溃,这比仅仅在滚动时变慢和“笨拙”要糟糕得多 =) - FoamyGuy
4个回答

7

Tim的评论非常准确。您在BaseAdapter.getView()方法中没有使用convertView,每次都会创建新视图,这是导致内存不足的主要原因。

据我所知,ListView将保存由getView()方法返回的所有视图在其内部的“回收站”容器中,只有当ListView与其窗口分离时才会被清除。这个“回收站”是它可以生成所有那些convertView并在适当时候供应给getView()的原因。

作为测试,您甚至可以注释掉为视图分配图像的代码部分:

                // final int imageResId = field.getInt(null);
                // imageView.setImageResource(imageResId);

你最终仍然会遇到内存分配失败的问题 :)


为什么ListView有回收站,而不是使用之前超出其范围的视图?这没有意义。你能展示一下它的代码中这种行为的证明吗?是否有一种方法可以禁用它或使用不同的视图,比如ListView? - android developer
1
正如我之前所述,这个回收站实际上是尝试重用那些现在已经超出边界的视图(通过将其作为 getView() 方法的 convertView 返回)。由于您的代码忽略了提供的 convertView,因此这种优化被浪费了。如果您感兴趣,可以在这里找到代码。只需搜索 mRecycler,您就会看到它是如何使用的。希望在查看后能更容易理解。 - Joe
那么当谷歌测试适配器的朴素版本(如他们演讲中所示的图表)时,他们是如何克服这个问题的呢?我的代码的整个重点是重新测试他们所做的事情,以便我可以教别人使用谷歌在演讲中展示的优化方法有多好。他们甚至在Nexus One上进行了测试... - android developer
1
感谢您提供这些信息。不幸的是,我仍然认为演示文稿中没有明确说明“您可以在10,000项ListView上使用愚笨的适配器而不会遇到内存问题”。当然,这可能只是我的问题,但我有我的理由(我已经在我的答案和评论中描述过了)。我也对回收站的引用部分有不同的理解(也许只是我)。让我们就此达成不同意的共识,好吗? :) - Joe
恭喜,太棒了 +1 - Lisa Anne
显示剩余5条评论

2

您的代码存在两个问题:

  1. 如前面的答案所述,您正在尝试创建太多新对象,这是导致OutOfMemory问题的主要原因。

  2. 您的代码不足以连续加载所有对象(例如向上/向下滑动进行滚动),这会导致卡顿。

以下是解决这两个常见问题的提示:

Field field = fields[position % fields.length];
View v = convertView;
ViewHolder holder = null;

if (v == null) {
    v = inflater.inflate(R.layout.list_item,null);
    holder = new ViewHolder();
    holder.Image = (ImageView) inflatedView.findViewById(R.id.imageView);
    holder.Text = (TextView)inflatedView.findViewById(R.id.textView);
    v.setTag(holder);
} else {
    holder = (ViewHolder) v.getTag();
}
return v;

这是用于高效的 ListView 的简单 ViewHolder
static class ViewHolder {   
    ImageView Image;
    TextView  Text;
}

相当简单但非常有效的编码。

1
我已经长时间遇到OOM错误,当我使用过多的图片填充我的ListView时(即使这些图片已经被压缩过)。在您的清单中使用以下内容可能会解决您的问题:
android:largeHeap="true"

这将为您的应用程序提供更大的内存空间。

仅在没有其他方法可以实现所需输出时使用此选项!

了解使用largeHeap的缺点,请查看答案


0
你正在捕获异常e,但OutOfMemoryError是错误(Error),而不是异常(Exception)。因此,如果你想要捕获OutOfMemory,你可以编写类似以下的代码:
catch(Throwable e){}

catch(OutOfMemoryError e){}

不错的提示,但它并没有回答问题。事实上,问题已经被回答了,我忘记打勾选它。 - android developer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接