Подтвердить что ты не робот

Нужна помощь, чтобы понять утечку памяти в приложении для Android

Мое приложение работает нормально, пока я не прерываю процесс инициализации при первом запуске после установки, выйдя и запуская приложение несколько раз, пока процесс инициализации еще не завершен. Логика обработки и AsyncTask могут справиться с этим довольно хорошо, поэтому я не получаю никаких несоответствий, но у меня проблема с кучей. Это становится все больше и больше, в то время как я делаю это тревожные выходы и запускает при настройке приложения, что приведет к ошибке OutOfMemory. Я уже нашел утечку, анализируя кучу с помощью MAT, но у меня все еще есть другая утечка, которую я еще не могу изолировать.
Для справочной информации: я храню контекст приложения, список и временную метку в статическом классе, чтобы иметь возможность получить к нему доступ из классов в любом месте приложения без использования утомительных пропущенных ссылок конструктором. Во всяком случае, должно быть что-то не так с этим статическим классом (ApplicationContext), поскольку он вызывает утечку памяти из-за списка зон. Объекты зоны обрабатываются данными GeoJSON. Вот как выглядит этот класс:

public class ApplicationContext extends Application {
    private static Context context;
    private static String timestamp;
    private static List<Zone> zones = new ArrayList<Zone>();

    public void onCreate()  {
        super.onCreate();
        ApplicationContext.context = getApplicationContext();
    }

    public static Context getAppContext() {
        return ApplicationContext.context;
    }

    public static List<Zone> getZones() {
        return zones;
    }

    public static void setData(String timestamp, List<Zone> zones) {
        ApplicationContext.timestamp = timestamp;
        ApplicationContext.zones = zones;
    }

    public static String getTimestamp() {
        return timestamp;
    }
}

Я уже пытался хранить такие зоны, как это

ApplicationContext.zones = новый ArrayList (зоны);

но это не повлияло. Я уже пытался поместить атрибут зон в другой статический класс, поскольку ApplicationContext загружается перед всеми другими классами (из-за записи в AndroidManifest), что может привести к такому поведению, но это тоже не проблема.

setData вызывается в моем "ProcessController" дважды. Один раз в doUpdateFromStorage и один раз в doUpdateFromUrl (String). Этот класс выглядит следующим образом:

public final class ProcessController {
    private HttpClient httpClient = new HttpClient();

    public final InitializationResult initializeData()  {
        String urlTimestamp;
        try {
            urlTimestamp = getTimestampDataFromUrl();

            if (isModelEmpty())  {
                if (storageFilesExist())  {
                    try {
                        String localTimestamp = getLocalTimestamp();

                        if (isStorageDataUpToDate(localTimestamp, urlTimestamp))  {
                            return doDataUpdateFromStorage();
                        } 
                        else  {
                            return doDataUpdateFromUrl(urlTimestamp);
                        }
                    } 
                    catch (IOException e) {
                        return new InitializationResult(false, Errors.cannotReadTimestampFile());
                    }
                }
                else  {
                    try {
                        createNewFiles();

                        return doDataUpdateFromUrl(urlTimestamp);
                    } 
                    catch (IOException e) {
                        return new InitializationResult(false, Errors.fileCreationFailed());
                    }
                }
            }
            else  {
                if (isApplicationContextDataUpToDate(urlTimestamp))  {
                    return new InitializationResult(true, "");  
                }
                else  {
                    return doDataUpdateFromUrl(urlTimestamp);
                }
            }
        } 
        catch (IOException e1) {
            return new InitializationResult(false, Errors.noTimestampConnection());
        }
    }

    private String getTimestampDataFromUrl() throws IOException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        return httpClient.getDataFromUrl(FileType.TIMESTAMP);
    }

    private String getJsonDataFromUrl() throws IOException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        return httpClient.getDataFromUrl(FileType.JSONDATA);
    }

    private String getLocalTimestamp() throws IOException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        return PersistenceManager.getFileData(FileType.TIMESTAMP);
    }

    private List<Zone> getLocalJsonData() throws IOException, ParseException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        return JsonStringParser.parse(PersistenceManager.getFileData(FileType.JSONDATA));
    }

    private InitializationResult doDataUpdateFromStorage() throws InterruptedIOException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        try {
            ApplicationContext.setData(getLocalTimestamp(), getLocalJsonData());

            return new InitializationResult(true, "");
        } 
        catch (IOException e) {
            return new InitializationResult(false, Errors.cannotReadJsonFile());
        } 
        catch (ParseException e) {
            return new InitializationResult(false, Errors.parseError());
        }
    }

    private InitializationResult doDataUpdateFromUrl(String urlTimestamp) throws InterruptedIOException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        String jsonData;
        List<Zone> zones;
        try {
            jsonData = getJsonDataFromUrl();
            zones = JsonStringParser.parse(jsonData);

            try {
                PersistenceManager.persist(jsonData, FileType.JSONDATA);
                PersistenceManager.persist(urlTimestamp, FileType.TIMESTAMP);

                ApplicationContext.setData(urlTimestamp, zones);

                return new InitializationResult(true, "");
            } 
            catch (IOException e) {
                return new InitializationResult(false, Errors.filePersistError());
            }
        } 
        catch (IOException e) {
            return new InitializationResult(false, Errors.noJsonConnection());
        } 
        catch (ParseException e) {
            return new InitializationResult(false, Errors.parseError());
        }
    }

    private boolean isModelEmpty()  {
        if (ApplicationContext.getZones() == null || ApplicationContext.getZones().isEmpty())  {    
            return true;
        }

        return false;
    }

    private boolean isApplicationContextDataUpToDate(String urlTimestamp) { 
        if (ApplicationContext.getTimestamp() == null)  {
            return false;
        }

        String localTimestamp = ApplicationContext.getTimestamp();

        if (!localTimestamp.equals(urlTimestamp))  {
            return false;
        }

        return true;
    }

    private boolean isStorageDataUpToDate(String localTimestamp, String urlTimestamp) { 
        if (localTimestamp.equals(urlTimestamp))  {
            return true;
        }

        return false;
    }

    private boolean storageFilesExist()  {
        return PersistenceManager.filesExist();
    }

    private void createNewFiles() throws IOException {
        PersistenceManager.createNewFiles();
    }
}

Может быть, это еще одна полезная информация, что этот ProcessController вызывается моей MainActivity AsyncTask при настройке приложения:

public class InitializationTask extends AsyncTask<Void, Void, InitializationResult> {
    private ProcessController processController = new ProcessController();
    private ProgressDialog progressDialog;
    private MainActivity mainActivity;
    private final String TAG = this.getClass().getSimpleName();

    public InitializationTask(MainActivity mainActivity) {
        this.mainActivity = mainActivity;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();

        ProcessNotification.setCancelled(false);

        progressDialog = new ProgressDialog(mainActivity);
        progressDialog.setMessage("Processing.\nPlease wait...");
        progressDialog.setIndeterminate(true); //means that the "loading amount" is not measured.
        progressDialog.setCancelable(true);
        progressDialog.show();
    };

    @Override
    protected InitializationResult doInBackground(Void... params) {
        return processController.initializeData();
    }

    @Override
    protected void onPostExecute(InitializationResult result) {
        super.onPostExecute(result);

        progressDialog.dismiss();

        if (result.isValid())  {
            mainActivity.finalizeSetup();
        }
        else  {
            AlertDialog.Builder dialog = new AlertDialog.Builder(mainActivity);
            dialog.setTitle("Error on initialization");
            dialog.setMessage(result.getReason());
            dialog.setPositiveButton("Ok",
                    new DialogInterface.OnClickListener() {

                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.cancel();

                            mainActivity.finish();
                        }
                    });

            dialog.show();
        }

        processController = null;
    }

    @Override
    protected void onCancelled() {
        super.onCancelled();

        Log.i(TAG, "onCancelled executed");
        Log.i(TAG, "set CancelNotification status to cancelled.");

        ProcessNotification.setCancelled(true);

        progressDialog.dismiss();

        try {
            Log.i(TAG, "clearing files");

            PersistenceManager.clearFiles();

            Log.i(TAG, "files cleared");
        } 
        catch (IOException e) {
            Log.e(TAG, "not able to clear files.");
        }

        processController = null;

        mainActivity.finish();
    }
}

Вот тело JSONParser. (UPDATE: я устанавливаю метод не статическим, но проблема сохраняется.) Я опускаю создания объектов из объектов JSON, так как не думаю, что это ошибка:

public class JsonStringParser {
    private static String TAG = JsonStringParser.class.getSimpleName();

    public static synchronized List<Zone> parse(String jsonString) throws ParseException, InterruptedIOException {
        JSONParser jsonParser = new JSONParser();

        Log.i(TAG, "start parsing JSON String with length " + ((jsonString != null) ? jsonString.length() : "null"));
          List<Zone> zones = new ArrayList<Zone>();

        //does a lot of JSON parsing here

        Log.i(TAG, "finished parsing JSON String");

        jsonParser = null;

        return zones;
    }
}

Вот куча дампа, который показывает проблему:

Memory chart

Это список деталей, который показывает, что эта проблема имеет какое-то отношение к арраисту.

detail

Любые идеи, что здесь неправильно? Btw: Я не знаю, что другая утечка, поскольку нет информации о деталях.

Возможно, важно. Эта диаграмма показывает статус, когда я не запускаю и не останавливаю приложение снова и снова. Это схема чистого старта. Но когда я начинаю и останавливаюсь несколько раз, это может привести к проблемам из-за нехватки места.

Вот диаграмма реального сбоя. Я начал и остановил приложение при инициализации несколько раз:

crash report

[UPDATE]
Я немного сузил его, не сохраняя контекст Android в моем классе ApplicationContext и делая PersistenceManager нестатичным. Проблема не изменилась, поэтому я абсолютно уверен, что это не связано с тем, что я храню Android-контекст во всем мире. Это все еще "Проблемный Подозреваемый 1" на графике выше. Поэтому я должен что-то сделать с этим огромным списком, но что? Я уже попытался его сериализовать, но неанализирующий этот список занимает гораздо больше времени, чем 20 секунд, поэтому это не вариант.

Теперь я попробовал что-то другое. Я вытолкнул весь ApplicationContext, поэтому у меня больше нет статических ссылок. Я попытался удерживать объекты ArrayList of Zone в MainActivity. Хотя я реорганизовал, по крайней мере, те части, которые мне нужно, чтобы приложение выполнялось, поэтому я даже не передал массив или активность всем классам, в которых он мне нужен, у меня все еще есть одна и та же проблема по-другому, поэтому я предполагаю, что объекты Зоны сами по себе являются проблемой. Или я не могу правильно прочитать кучу кучи. См. Новые графики ниже. Это результат простого запуска приложения без помех.

[UPDATE]
Я пришел к выводу, что утечки памяти нет, потому что "память накапливается в одном экземпляре" не похожа на утечку. Проблема в том, что запуск и остановка снова и снова запускает новые AsyncTasks, как видно на одном графике, поэтому решение должно состоять в том, чтобы не запускать новую AsyncTask. Я нашел возможное решение для SO, но он пока не работает для меня.

memory error 4memory error 5

4b9b3361

Ответ 1

МОЕ ЗАКЛЮЧИТЕЛЬНОЕ РЕШЕНИЕ

Я нашел решение самостоятельно. Он работает стабильно и не создает утечки памяти, когда я запускаю и останавливаю приложение много раз. Другим преимуществом этого решения является то, что я смог выгнать все эти части ProcessNotification.isCancelled().

Ключ должен содержать ссылку на мою InitializationTask в моем ApplicationContext. При таком подходе я могу возобновить работу AsyncTask в новом MainActivity, когда я начну новый. Это означает, что я никогда не запускаю более одного AsyncTask, но я привязываю каждый новый экземпляр MainActivity к текущей выполняемой задаче. Старая активность будет отключена. Это выглядит так:

новые методы в ApplicationContext:

public static void register(InitializationTask initializationTask) {
    ApplicationContext.initializationTask = initializationTask;
}

public static void unregisterInitializationTask()  { 
    initializationTask = null;
}

public static InitializationTask getInitializationTask() {
    return initializationTask;
}

MainActivity
 (Я должен поставить здесь progressDialog, иначе он не будет показан, если я остановлюсь и начну новое действие):

@Override
protected void onStart() {
    super.onStart();

    progressDialog = new ProgressDialog(this);
    progressDialog.setMessage("Processing.\nPlease wait...");
    progressDialog.setIndeterminate(true); // means that the "loading amount" is not measured.
    progressDialog.setCancelable(true);
    progressDialog.show();

    if (ApplicationContext.getInitializationTask() == null) {
        initializationTask = new InitializationTask();
        initializationTask.attach(this);

        ApplicationContext.register(initializationTask);

        initializationTask.execute((Void[]) null);
    } 
    else {
        initializationTask = ApplicationContext.getInitializationTask();

        initializationTask.attach(this);
    }
}

MainActivity "onPause" содержит initializationTask.detach(); и progressDialog.dismiss();. finalizeSetup(); также отклоняет диалог.

ИнициализацияTask содержит еще два метода:

public void attach(MainActivity mainActivity) {
    this.mainActivity = mainActivity;
}

public void detach() {
    mainActivity = null;
}

onPostExecute задачи вызывает ApplicationContext.unregisterInitializationTask();.

Ответ 2

Прежде всего, я должен согласиться с Эмилем:

"Бесконечные пропуски ссылок конструктором" - это то, что помогает избежать такие вопросы. Честно говоря, использование статики таким образом, безусловно, одно способ создания утечек памяти, как это, особенно со статическим ссылку на ваш контекст.

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

То, что также привлекло мое внимание, обратите внимание, что метод onCancelled AsyncTask не будет вызываться до завершения doInBackground. Таким образом, ваш глобальный флаг отмены (ProcessNotification.isCancelled()) более или менее бесполезен (если используется только в показанных фрагментах кода).

Также из опубликованных изображений памяти список zones содержит только "31". Сколько он должен удерживать? Насколько он увеличивается? Если он действительно увеличится, он может быть в методе JsonStringParser.parse, который снова будет static. Если он содержит список элементов в каком-то кеше, и логика управления работает некорректно (например, при одновременном доступе нескольких потоков), он может добавлять элементы в этот кеш при каждом вызове.

  • Угадайте 1: поскольку метод разбора static, эти данные не очищаются (обязательно) при закрытии приложения. static инициализируются один раз и для целей этого случая никогда не деинируются до тех пор, пока процесс (физический vm-) не будет остановлен. Android не гарантирует, что процесс будет убит, даже если приложение будет остановлено (см., Например, замечательное объяснение здесь). Поэтому вы можете накапливать некоторые данные в некоторой части static вашего (возможно, синтаксического) кода.
  • Угадай 2: Поскольку вы повторно запускаете свое приложение несколько раз, у вас есть фоновый поток, выполняемый несколько раз параллельно (предположение: каждый раз, когда вы перезапускаете приложение, создается новый поток. Обратите внимание, что ваш код не показывает защиты от это.) Этот первый синтаксический анализ все еще запущен, другой запускается, поскольку глобальные переменные zones по-прежнему не содержат значений. Глобальная функция parse может не быть потокобезопасной и несколько раз вводить несколько данных в список, который в конечном итоге возвращается, что дает больший и больший список. Опять же этого обычно избегают, не имея методов static (и помните о многопоточности).

(Код не является полным, поэтому догадывается, там могут быть и другие вещи, скрывающиеся там.)

Ответ 3

Внутри вашей AsyncTask у вас есть ссылка на Контекст: MainActivity. Когда вы запустите несколько AsyncTask, они будут поставлены в очередь с помощью ExecutorService. Таким образом, все AsyncTask, если они будут длительными, будут "живыми" (а не сбор мусора). И каждый из них сохранит ссылку на Activity. Следовательно, все ваши действия также будут сохранены в живых.

Это настоящая утечка памяти, так как Android захочет, чтобы мусор собирал активность, которая больше не отображается. И ваши AsyncTasks предотвратят это. Все действия хранятся в памяти.

Я рекомендую вам попробовать RoboSpice Motivations, чтобы узнать больше об этой проблеме. В этом приложении мы объясняем, почему вы не должны использовать AsyncTasks для длительных операций. Есть еще несколько работ, которые позволяют вам использовать их, но их сложно реализовать.

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

Собственно, RoboSpice - это библиотека, которая позволяет выполнять сетевые запросы внутри службы. Этот подход довольно интересен, если он создаст контекст (услугу), который не связан с вашими действиями. Таким образом, ваш запрос может занять столько времени, сколько захочет, и не будет мешать работе коллекции мусора Android.

Существует два модуля RoboSpice, которые вы можете использовать для обработки запроса REST. Один для Spring Android, а другой для Google Http Java Client. Обе библиотеки облегчат разбор JSON.

Ответ 4

Я предполагаю, что вы установили ссылку на MainActivity, но я хотел бы упомянуть еще одну проблему...

Вы заявляете, что синтаксический анализ занимает 20 секунд. И если вы "прервите" приложение, эта обработка не исчезнет.

Из кода, который вы здесь показываете, кажется, что 99% этого 20 секунд потрачено внутри JsonStringParser.parse().

Если я посмотрю на ваш комментарий, "здесь много разборок JSON", я предполагаю, что ваше приложение делает вызов в JSONParser.something(), который остается на 20 секунд. Несмотря на то, что JsonStringParser статичен, каждый вызов JsonStringParser.parse() создает новую копию JSONParser(), и я предполагаю, что использует много памяти.

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

Итак, я думаю, что ваша первопричина заключается в том, что вы запускаете вторую (или третью или четвертую) копию JSONParser.something(), потому что каждый из них будет выполняться независимо и попытаться выделить много блоков памяти и остаться равномерным дольше 20 секунд, потому что им придется делиться циклами процессора. Объединенное распределение памяти нескольких объектов JSONParser - это то, что убивает вашу систему.

Подводя итог:

  • Не запускайте другой JsonStringParser.parse() до первого убит или завершен.
  • Это означает, что вы должны найти способ остановить JsonStringParser.parse() когда вы "прерываете" приложение или повторно используете текущую копию, когда вы перезапустите приложение.

Ответ 5

Динк, я вижу, как это возможно, мои глаза все-таки скрестились.

Убедитесь, что вы не загружаете данные из своего локального хранилища, добавляете к нему больше данных и затем сохраняете их обратно на локальный диск.

Что-то вокруг следующих методов в сочетании с другими частями вашей программы.

Если было вызвано следующее, и по какой-то причине вы вызываете getDatafromURL, то я считаю, что вы постоянно наращиваете свой набор данных.

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

ApplicationContext.setData(getLocalTimestamp(), getLocalJsonData());

private List<Zone> getLocalJsonData() throws IOException, ParseException {
    if (ProcessNotification.isCancelled()) {
        throw new InterruptedIOException();
    }

    return JsonStringParser.parse(PersistenceManager.getFileData(FileType.JSONDATA));
}

В противном случае я думаю, что проблема заключается в вашем коде Parsing или, возможно, в одном из статических классов, которые вы используете для сохранения данных.