Android: преобразование потока в строку без исчерпания памяти - PullRequest
12 голосов
/ 19 января 2011

У меня есть клиент Android, который связывается с сервером через конечные точки REST-ful и JSON.Из-за этого мне нужно получить полный ответ сервера перед преобразованием его в хэш.У меня есть этот код для этого (где-то в Интернете):

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 ]

3 голосов
/ 19 января 2011

В общем случае ответ отрицательный, но вы, безусловно, можете настроить условия, из-за которых у вас заканчивается память. В частности, если вы отправите длину строки перед вашим потоком, вы сможете создать StringBuilder с правильным размером массива внутри него. Размеры массивов после создания не могут быть изменены, поэтому, если у вас заканчивается емкость массива в StringBuilder, реализация должна выделить новый массив (обычно в два раза больше, чтобы избежать слишком большого изменения размера), а затем скопировать содержимое старого массива. Рассмотрим поток размером X, чтобы изменить размер StringBuilder, который оказался емкостью X-1, вам нужно почти X * 3 объема памяти. Размер StringBuilder, позволяющий избежать изменения размеров, позволит вам втиснуть в память большие потоки.

Еще одна вещь, которую вы можете захотеть сделать, это настроить объем памяти, доступный для процесса вашего сервера. Используйте переключатель, например -Xmx1024m, при запуске процесса сервера.

Конечно, было бы намного лучше пересмотреть ваш алгоритм, чтобы не требовать, чтобы весь поток хранился в памяти. Это позволит вам обрабатывать больше клиентов с одинаковым количеством оборудования.

2 голосов
/ 19 января 2011

Android имеет ограничения на максимальный объем памяти, который вы можете выделить для вашего приложения. Вы могли бы рассмотреть чтение потока на лету и не сохранять все это в строку, если ответ очень велик. Но вы должны рассмотреть следующие лучшие практики.

Вы должны хранить данные в базе данных sqlite или в обычном файле. Не рекомендуется делать то, что вы делаете, поскольку пользователь может нажать кнопку «Домой» или получить телефонный звонок, когда вы находитесь в процессе сохранения ответа. Лучше использовать базу данных, чтобы вы могли вернуться в состояние, в котором вас прервали. Тогда вам также не нужно беспокоиться об исчерпании памяти.

Смотрели ли вы этот доклад о передовых методах взаимодействия со службами REST с Android? http://www.youtube.com/watch?v=xHXn3Kg2IQE?8m50s (8:50 и 11:20). Настоятельно рекомендуется разъяснить лучшие практики и почему не следует извлекать данные REST без использования базы данных.

Короче говоря, рассмотрите возможность сохранения в базу данных sqlite или файл. Если данные очень велики, вы можете их сжать перед сохранением.

1 голос
/ 03 февраля 2012

Может быть, этот код помогает избежать использования 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){}
}
1 голос
/ 19 января 2011

Существуют разные способы решения подобных проблем. Одним из способов может быть использование хэш-функции, которая не требует, чтобы весь поток находился в памяти, то есть вы предоставляете ему символ или блок символов одновременно. Другой - уменьшить размер ответа.

Если вы не можете сделать это и вам нужен весь поток, тогда я бы не стал использовать readLine () и просто вызывал read () в буферизованном входном потоке и добавлял символ, полученный из read, в построитель строк. Это значительно сократит количество строк, которые вы создаете и отбрасываете. (Простая оптимизация приведенного выше кода заключается в удалении новой строки в вызове append () - там вы также создаете другую строку без необходимости.) Кроме того, если у вас есть представление о том, какой длины будет полученная строка, вы также может установить начальную емкость строителя строк при создании, чтобы вы сразу знали, не хватит ли вам памяти.

Как только вы выйдете за пределы этого, вы должны начать разбивать строку на блоки, которые вы храните в файловой системе .... довольно быстро усложняется.

0 голосов
/ 08 июля 2013

Вы пробовали встроенный метод для преобразования потока в строку?Это часть библиотеки 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 можно скачать здесь: http://commons.apache.org/io/download_io.cgi

...