Android无限列表内存管理

20
我正在实现一个无限滚动的列表视图,通过在onScrollStateChanged(...)方法中向arraylist加载更多项。 如果我为获取超过100万条数据实现此方案,那么将有一百万个对象添加到arraylist中,这会占用大量内存。 有哪些方案可以用于有效的内存管理? PS:问题涉及到可放入适配器中的项目数量。 编辑: 更多细节: 数据来源于互联网。 我必须从互联网上获取数据并将其放入listview的适配器中。

1
关于ListView本身,你应该利用它的回收机制:https://dev59.com/5Gct5IYBdhLWcg3wn-xz#14108676。 - super-qua
@super-qua - 我理解视图中的回收处理。但问题是关于listview的来源,它是一个arraylist。 - user3388324
是的,我知道,只是想提醒你以防你不知道。 - super-qua
为什么要将一百万个项全部加载到ArrayList中,如果你只是懒惰地加载列表视图(并且由于ListView本身会回收视图)?我建议仅在内存中保留屏幕上显示的项目,并动态加载新项目。当然,我不知道你的数据来源,这可能不那么容易。如果你详细说明实际数据,我可以给你一些建议。 - Emanuel Moecklin
3
考虑将数据存储到 SQLite 数据库中,并使用 Cursor(使用 CursorAdapter)填充 ListView。更具体地说,好处是 SQLiteCursor 扩展自 AbstractWindowedCursor,后者通过 CursorWindow(基本上是缓冲区)公开数据来利用。因此,您不必太担心在内存中存储大量项目,并为所有这些不同的 Android 设备进行适当的管理。话虽如此,您真的想向用户显示超过一百万个项目的列表吗? - MH.
显示剩余2条评论
9个回答

11

我认为你应该保留当前的条目以及它们之前或之后的一个(可能是100个),然后将这些数据放入缓存中。

当你滚动列表视图时,获取更多的条目并像之前一样更新缓存(不要一次获取100万条)。


如果我有一个包含100个元素的适配器,在加载第101个元素时,你的意思是应该用第101个元素替换第一个条目? - user3388324
1
使用缓冲区思想。我的意思是将101到200放入缓存中(当前为100),如果您滚动到130,则无需下载任何内容,当当前为160时,可以在后台加载201到300。只需在一些空闲时间更新缓存即可。 - tianwei
我猜这是适用于理论无限数据集(例如社交网络流)的方法。 - rciovati
你能否提供一个实际的代码示例来说明这个建议? - Arsenius

3
在Android中,ListView是虚拟化的。实际上,这意味着它内部没有元素数量的实际限制。你可以将数百万行放入列表中,它只会为当前可见的行(或最多几行)分配内存。 来源 此外,请查看此文章Android的ListView性能技巧

我理解ListView中的优化。问题是有多少对象可以放入ListView的适配器中。 - user3388324
你有一个设备特定的内存限制。只要你不耗尽内存限制,你就可以存储任意多的项。 - fida1989

2
这个问题与“适配器”“容量”无关,而是与您的应用程序分配的内存量有关。
它有一个保留的,以便分配对象,如果超出此限制,您将收到内存不足异常
这里是一个小测试,它可以让您了解您可以分配的数据量。但请注意,在此示例中,对象仅包含一个字符串,如果是华丽的位图,要分配的对象数量会少得多得多。
//MemoryActivity
public class MemoryActivity extends Activity {

    private List<TestObject> _testObjects = new ArrayList<TestObject>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test_memory);
        coolGuysDoNotLookBackAtExplosion();
        starCountdown();
    }

    private void starCountdown() {
        new CountDownTimer(300000, 500) {

            public void onTick(long millisUntilFinished) {
                TextView tv_watcher = (TextView) findViewById(R.id.tv_watcher);
                tv_watcher.setText(getMemoryUsage());
            }

            public void onFinish() {
                starCountdown();
            }

        }.start();
    }

    private String getMemoryUsage() {
        String heapSize = String.format("%.3f", (float) (Runtime.getRuntime().totalMemory() / 1024.00 / 1024.00));
        String freeMemory = String.format("%.3f", (float) (Runtime.getRuntime().freeMemory() / 1024.00 / 1024.00));

        String allocatedMemory = String
                .format("%.3f", (float) ((Runtime.getRuntime()
                        .totalMemory() - Runtime.getRuntime()
                        .freeMemory()) / 1024.00 / 1024.00));
        String heapSizeLimit = String.format("%.3f", (float) (Runtime.getRuntime().maxMemory() / 1024.00 / 1024.00));

        String nObjects = "Objects Allocated: " + _testObjects.size();

        return "Current Heap Size: "    + heapSize
                + "\n Free memory: "
                + freeMemory
                + "\n Allocated Memory: "
                + allocatedMemory
                + "\n Heap Size Limit:  "
                + heapSizeLimit
                + "\n" + nObjects;
    }

    private void coolGuysDoNotLookBackAtExplosion(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                _testObjects = new ArrayList<TestObject>();
                while (true) {
                    _testObjects.add(new TestObject());
                }
            }
        }).start();
    }
}

//测试对象
public class TestObject {
    private String sampleText = "Lorem Ipsum is simply dummy text of the printing and typesetting industry";
}

1
你应该将分页加载更多按钮作为ListView的页脚。例如:

url = http://your_full_url.php?page=1

假设每页有100个记录,那么第一次获取第1页的全部100条记录,将它们显示在ListView上并进行缓存。现在滚动ListView并点击加载更多按钮(加载更多按钮应设置为ListView的页脚)。

当你点击加载更多时,将通过调用

url = http://your_full_url.php?page=2等来获取下一个100条记录

url = http://your_full_url.php?page=3,

url = http://your_full_url.php?page=4等等...

每次都会缓存这些记录,以便在连接丢失的情况下可以显示缓存中可用的记录。


1
如果您的ListView只包含文本项,则不需要做太多工作。但是,如果您正在加载更多内存密集的内容,例如可绘制对象(例如,在视图的右侧有一张图片),那么为了获得最佳结果,您应该进行一些回收。在较弱的设备上,您可能很快会收到OutOfMemoryException。即使在Nexus 4上也可能出现这种情况。只需尝试快速滚动,向上和向下,反复执行,直到强制关闭。

请查看RecyclerListener,它非常容易实现。


0

我在文档中没有找到确切的数字。然而,所有Adapter#getCount()(查看子类)的返回类型都是int

因此,我们可以强烈怀疑您可以将最多Integer.MAX_VALUE个项目添加到适配器中,这是231-1(超过20亿)。适配器使用ListsMaps来在内部存储数据,它们具有相同的限制

因此,您不应该担心适配器的限制,而是使用太多内存。我建议您将10-100个元素加载到适配器中,并在用户到达列表视图底部时简单地添加更多项目。


0

天威的方法是正确的。

如果ListView是懒加载的,而且由于ListView本身正在回收视图,因此最好只在内存中保留可见的列表条目。您基本上在适配器中执行与ListView对视图相同的操作。

如果您将所有数据都保存在内存中,那么懒加载ListView的意义何在?只需加载所有数据并跳过懒加载部分即可...当然,使用懒加载方法仅加载可见数据(以及可能更多数据),您必须在列表底部和顶部实现懒加载才能使其正常工作。

现在,由于没有关于数据(文本、图像)或来源(Internet、SQLite Db、文本文件...)的信息,我无法为您提供如何实现此操作的代码(示例)。如果您详细说明数据,我可以更准确地回答问题。


0

我猜测是sqlite数据库和流解析器(GSON)。


1
你能再解释一下吗?答案很模糊,让我感到一无所知。 - user3388324
根据您的问题,您不希望使用ArrayList,因为它会占用更多的内存。因此,我建议您使用SQLite数据库,并在需要使用该信息时使用ContentProvider。 - Vaibhav Ajay Gupta
流式解析器逐一解析数据流,因此无需一次性将数百万条目下载到设备内存中。逐一进行并保存到数据库中。这样可以节省大量内存。ListView本身已经高度优化,它只使用所需的内存。因此,使用流式解析器逐一下载数据并将其逐一保存到数据库中,然后将该数据库连接到listview即可。无需在内存缓存中存储任何内容。 - Vaibhav Ajay Gupta
让我试试。我猜问题可能在于sqllite数据库的大小。 - user3388324

-1
如果您需要在内存中保留1M个对象,并且假设对象数据很小,那么这只是几MB的内存,应该可以直接保留在内存中。从问题中我理解到,当用户向前滚动时,您将读取更多的项目,因此在实践中,您不会有1M行 - 用户需要长时间滚动才能到达1M。 只要您正确使用ListView,就可以使适配器数据在内存中增长到1M+行而没有任何问题。

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