Android:将Stream转换为String而不会耗尽内存

时间:2011-01-18 21:21:16

标签: java android json out-of-memory

我有一个Android客户端,通过REST-ful端点和JSON与服务器通信。因此,我需要在将其转换为Hash之前检索完整的服务器响应。我有这个代码(在互联网上找到的地方):

private static String convertStreamToString(InputStream is) {

    BufferedReader reader = new BufferedReader(new InputStreamReader(is));
    StringBuilder sb = new StringBuilder();

    String line = null;
    try {
        while ((line = reader.readLine()) != null) {
            sb.append(line + "\n");
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return sb.toString();
}

代码在很大程度上起作用,但是我看到来自具有OutOfMemory异常的客户的字段中的崩溃报告:

    while ((line = reader.readLine()) != null) {

完整堆栈跟踪是:

java.lang.RuntimeException: An error occured while executing doInBackground()
    at android.os.AsyncTask$3.done(AsyncTask.java:200)
    at java.util.concurrent.FutureTask$Sync.innerSetException(FutureTask.java:273)
    at java.util.concurrent.FutureTask.setException(FutureTask.java:124)
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:307)
    at java.util.concurrent.FutureTask.run(FutureTask.java:137)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1068)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:561)
    at java.lang.Thread.run(Thread.java:1102)
Caused by: java.lang.OutOfMemoryError
    at java.lang.String.(String.java:468)
    at java.lang.AbstractStringBuilder.toString(AbstractStringBuilder.java:659)
    at java.lang.StringBuilder.toString(StringBuilder.java:664)
    at java.io.BufferedReader.readLine(BufferedReader.java:448)
    at com.appspot.myapp.util.RestClient.convertStreamToString(RestClient.java:303)
    at com.appspot.myapp.util.RestClient.executeRequest(RestClient.java:281)
    at com.appspot.myapp.util.RestClient.Execute(RestClient.java:178)
    at com.appspot.myapp.$LoadProfilesTask.doInBackground(GridViewActivity.java:1178)
    at com.appspot.myapp.$LoadProfilesTask.doInBackground(GridViewActivity.java:1)
    at android.os.AsyncTask$2.call(AsyncTask.java:185)
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)
    ... 4 more

我的问题:除了从服务器发送较小的数据块之外,还有什么方法可以解决这个问题吗?

谢谢!

5 个答案:

答案 0 :(得分:3)

一般来说,答案是否定的,但你当然可以调整导致内存不足的条件。特别是,如果您在流之前发送字符串长度,您将能够在其中创建一个具有正确数组大小的StringBuilder。数组在创建后无法调整大小,因此如果在StringBuilder中耗尽数组容量,实现必须分配一个新数组(通常是两倍大小以避免过多调整大小),然后复制旧数组内容。考虑大小为X的流,为了调整刚好恰好是X-1容量的StringBuilder,你需要几乎X * 3的内存量。调整StringBuilder的大小以避免调整大小将允许您将更大的流压缩到内存中。

您可能想要做的另一件事是调整服务器进程可用的内存量。启动服务器进程时,请使用-Xmx1024m之类的开关。

当然,将您的算法修改为不要求将整个流保存在内存中会好得多。它将使您能够使用相同数量的硬件处理更多客户端。

答案 1 :(得分:2)

Android限制了您可以分配给应用程序的最大内存量。您可以考虑快速读取流 ,如果响应非常大,则不要将整个事物保存到String中。但你应该考虑遵循最佳实践。

您应该将数据存储在sqlite数据库或常规文件中。当您正在保存响应时,用户可能会按下主页按钮或接听电话,这不是最佳做法。最好使用数据库,以便您可以回到被中断的状态。那么你也不必担心内存不足。

您是否看过有关与Android通信的最佳实践的讨论? http://www.youtube.com/watch?v=xHXn3Kg2IQE?8m50s(8:50和11:20)。强烈建议清除最佳实践以及为什么不使用数据库就不应检索REST数据。

简而言之,请考虑保存到sqlite数据库或文件。如果数据非常大,您可以考虑在存储之前对其进行压缩。

答案 2 :(得分:1)

解决这些问题的方法有很多种。一种方法是使用不需要整个流存储在内存中的散列函数,即一次为它提供一个字符或一个字符块。另一个是减少响应的大小。

如果你不能这样做并且你需要整个流,那么我将避免使用readLine()并在缓冲的输入流上调用read()并将你从read中获得的字符追加到字符串构建器。这将减少您正在创建和放弃的字符串数量。 (对上面代码的简单优化是在append()调用中取出换行符 - 你也在那里不必要地创建另一个字符串。)另外,如果你知道结果字符串有多长,你还可以在构造时设置字符串构建器的初始容量,以便您立即知道是否内存不足。

一旦你超越了,你就必须开始将字符串拆分成存储在文件系统上的块....复杂得快。

答案 3 :(得分:1)

也许这段代码有助于避免使用StringBuilder和内存不足错误:

private String convertStreamToString(InputStream is) {
    ByteArrayOutputStream oas = new ByteArrayOutputStream();
    copyStream(is, oas);
    String t = oas.toString();
    try {
        oas.close();
        oas = null;
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return t;
}

private void copyStream(InputStream is, OutputStream os)
{
    final int buffer_size = 1024;
    try
    {
        byte[] bytes=new byte[buffer_size];
        for(;;)
        {
          int count=is.read(bytes, 0, buffer_size);
          if(count==-1)
              break;
          os.write(bytes, 0, count);
        }
    }
    catch(Exception ex){}
}

答案 4 :(得分:0)

您是否尝试过内置方法将流转换为字符串?它是Apache Commons库的一部分(org.apache.commons.io.IOUtils)。

那么你的代码就是这一行:

String total = IOUtils.toString(inputStream);

可在此处找到相关文档:http://commons.apache.org/io/api-1.4/org/apache/commons/io/IOUtils.html#toString%28java.io.InputStream%29

可以从这里下载Apache Commons IO库:http://commons.apache.org/io/download_io.cgi