- Свежий взгляд на отображение диалогов в Android
- Решение «в лоб»
- Устаревший способ
- Способ из документации
- Поиск идеального решения
- Реактивный способ
- Пишите диалоги правильно
- Утечка памяти
- Утечка через слушателей
- Пример утечки с внутренним классом
- Пример утечки с анонимным классом
- Утечка через потоки
- Singleton (Одиночка)
- Утечка с таймерами
- Фрагменты
- Пример утечки с системными менеджерами
Свежий взгляд на отображение диалогов в Android
На картинке первая мысль читателя, который недоумевает, что можно написать про такую простую задачу как отображения диалога. Аналогично думает и менеджер: «Тут ничего сложного, наш Вася за 5 минут сделает». Я, конечно, утрирую, но на самом деле всё не так просто, как кажется на первый взгляд. Особенно если мы говорим про Android.
Итак, на дворе шёл 2019 год, а мы всё ещё не умеем нормально показывать диалоги.
Давайте всё по порядку, и начнем с постановки задачи:
Требуется показать простой диалог с текстом для подтверждения действия и кнопками «подтвердить/отмена». По нажатию на кнопку «подтвердить» — совершить действие, по кнопке «отмена» — закрыть диалог.
Решение «в лоб»
Я бы назвал этот способ джуниорским, потому что не первый раз сталкиваюсь с непониманием, почему нельзя просто использовать AlertDialog, как показано ниже:
Довольно распространенный способ для начинающего разработчика, он очевиден и интуитивно понятен. Но, как и во многих случаях при работе с Android, этот способ совершенно неправильный. На ровном месте мы получаем утечку памяти, достаточно повернуть устройство, и вы увидете в логах такую ошибку:
На Stackoverflow вопрос по этой проблеме один из самых популярных. Если коротко, то проблема в том, что мы либо показываем диалог, либо не закрываем диалог после завершения работы активити.
Можно, конечно, вызывать dismiss у диалога в onPause или onDestroy активити, как советуют в ответе по ссылке. Но это не совсем то, что нам нужно. Мы хотим, чтобы диалог восстанавливался после поворота устройства.
Устаревший способ
До появления фрагментов в Android диалоги должны были отображаться через вызов метода активити showDialog. В этом случае активити правильно управляет жизненным циклом диалога и восстанавливает его после поворота. Создание самого диалога нужно было реализовать в коллбэке onCreateDialog:
Не очень удобно, что приходится заводить идентификатор диалога и передавать параметры через Bundle. И мы все ещё можем получить проблему «leaked window», если попытаемся отобразить диалог после вызова onDestroy у активити. Такое возможно, например, при попытке показать ошибку после асинхронной операции.
Вообще, эта проблема типична для Android, когда нужно что-то сделать после асинхронной операции, а активити или фрагмент уже уничтожен в этот момент. Наверное, поэтому MV*-паттерны более популярны в Android-сообществе, чем среди iOS-разработчиков.
Способ из документации
В Android Honeycomb появились фрагменты, и описанный выше способ устарел, а метод showDialog у активити помечен как deprecated. Нет, AlertDialog не устарел, как ошибаются многие. Просто теперь появился DialogFragment, который оборачивает объект диалога и управляет его жизненным циклом.
Родные фрагменты тоже устарели начиная с 28 API. Теперь следует использовать только реализацию из Support Library(AndroidX).
Давайте реализуем нашу задачу, как это предписывает официальная документация:
- Для начала нужно наследоваться от DialogFragment и реализовать создание диалога в методе onCreateDialog.
- Описать интерфейс событий диалога и инстанцировать слушатель в методе onAttach.
- Реализовать интерфейс событий диалога в активити или фрагменте.
Если читателю не очень понятно, почему нельзя передавать слушатель через конструктор, то он может почитать подробнее об этом тут
Код фрагмента диалога:
Достаточно много кода получилось, не так ли?
Как правило, в проекте есть какой-нибудь MVP, но я решил, что вызовы презентера можно опустить в данном случае. В примере выше стоит ещё добавить статический метод создания диалога newInstance и передачу параметров в аргументы фрагмента, всё как полагается.
И это всё ради того, чтобы диалог вовремя скрывался и правильно восстанавливался. Не удивительно, что появляются такие вопросы на Stackoverflow: один и два.
Поиск идеального решения
Текущее положение дел нас не устраивало, и мы стали искать способ, как сделать работу с диалогами более комфортной. Было ощущение, что можно сделать проще, почти как в первом способе.
Ниже сформулированы соображения, которыми мы руководствовались:
- Нужно ли сохранять и восстанавливать диалог после убийства процесса приложения?
В большинстве случаев это не требуется, как и в нашем примере, когда нужно показать простое сообщение или что-то спросить. Такой диалог актуален пока не потеряно внимание пользователя. Если его восстановить после долгого отсутствия в приложении, то пользователь потеряет контекст с планируемым действием. Поэтому нужно только поддержать повороты устройства и правильно обрабатывать жизненный цикл диалога. Иначе от неловкого движения устройства пользователь может потерять только что открытое сообщение, не прочитав его. - При использовании DialogFragment появляется слишком много boilerplate-кода, теряется простота. Поэтому было бы неплохо избавиться от фрагмента как обёртки и использовать Dialog напрямую. Для этого придется хранить состояние диалога, чтобы показать его вновь после пересоздания View и скрывать, когда View умирает.
- Все привыкли воспринимать показ диалога как команду, особенно если работаешь только с MVP. Задачу последующего восстановление состояния берет на себя FragmentManager. Но можно посмотреть на эту ситуацию иначе и начать воспринимать диалог как state. Это намного удобнее при работе с паттернами PM или MVVM.
- Учитывая, что большинство приложений сейчас используют реактивные подходы, появляется потребность в том, чтобы диалоги были реактивными. Основная задача — не разрывать цепочку, которая инициирует показ диалога, и привязать реактивный поток событий для получения результата от него. Это очень удобно на стороне PresentationModel/ViewModel, когда манипулируешь несколькими потоками данных.
Мы учли все вышеописанные требования и придумали способ реактивного показа диалогов, который успешно реализовали в нашей библиотеке RxPM (про нее есть отдельная статья).
Само решение не требует библиотеки и может быть сделано отдельно. Руководствуясь идеей «диалог как state» можно попробовать построить решение на основе модных ViewModel и LiveData. Но я оставлю это право за читателем, а далее речь пойдет уже о готовом решении из библиотеки.
Реактивный способ
Я покажу, как исходная задача решается в RxPM, но сначала пару слов о ключевых понятиях из библиотеки:
- PresentationModel — хранит реактивный стейт, содержит UI-логику, переживает повороты.
- State — реактивный стейт. Можно воспринимать как обертку над BehaviorRelay.
- Action — обертка над PublishRelay, служит для передачи событий от View в PresentationModel.
- State и Action имеют observable и consumer.
За состояние диалога отвечает класс DialogControl. Он имеет два параметра: первый для типа данных, которые должны отображаться в диалоге, второй — для типа результата. В нашем примере тип данных будет Unit, но это может быть сообщение пользователю или любой другой тип.
В DialogControl есть следующие методы:
- show(data: T) — просто отдает команду на отображение.
- showForResult(data: T): Maybe — показывает диалог и открывает поток для получения результата.
- sendResult(result: R) — отправляет результат, вызывается со стороны View.
- dismiss() — просто скрывает диалог.
В DialogControl хранится состояние — есть диалог на экране или нет (Displayed/Absent). Вот так это выглядит в коде класса:
Создадим простую PresentationModel:
Обратите внимание, что обработка кликов, получение подтверждения и обработка действия реализованы в одной цепочке. Это позволяет сделать код сфокусированным и не раскидывать логику по нескольким коллбэкам.
Далее просто привязываем DialogControl во View с помощью экстеншена bindTo.
Собираем обычный AlertDialog, а результат отправляем через sendResult:
При типичном сценарии под капотом происходит примерно следующее:
- Кликаем на кнопку, событие через Action «buttonClicks» попадает в PresentationModel.
- По этому событию запускаем отображение диалога через вызов showForResult.
- В результате состояние в DialogControl меняется с Absent на Displayed.
- При получении события Displayed — вызывается лямбда, которую мы передали в привязке bindTo. В ней создается объект диалога, который затем показывается.
- Пользователь нажимает, кнопку «Confirm», срабатывает слушатель и результат нажатия отправляется в DialogControl посредством вызова sendResult.
- Далее результат попадает во внутренний Action «result», а состояние с Displayed меняется на Absent.
- При получении события Absent текущий диалог закрывается.
- Событие от Action «result» попадает в поток, который был открыт вызовом showForResult и обрабатывается цепочкой в PresentationModel.
Стоит отметить, что диалог закрывается и в момент, когда View отвязывается от PresentationModel. В этом случае состояние остается Displayed. Оно будет получено при следующей привязке и диалог будет восстановлен.
Как видите, необходимость в DialogFragment пропала. Диалог показывается, когда View привязывается к PresentationModel и скрывается, когда View отвязывается. За счёт того, что состояние хранится в DialogControl, который в свою очередь хранится в PresentationModel, диалог восстанавливается после поворота устройства.
Пишите диалоги правильно
Мы с вами рассмотрели несколько способов отображения диалогов. Если вы все ещё показываете первым способом, то прошу вас, не делайте больше так. Для любителей MVP ничего не остается, как использовать стандартный способ, который описан в официальной документации. К сожалению, склонность к императивности этого паттерна не позволяет сделать по-другому. Ну, а фанатам RxJava рекомендую присмотреться к реактивному способу и нашей библиотеке RxPM.
Источник
Утечка памяти
Когда мы пишем код, то создаём различные объекты, которые занимают память. Когда объект нам не нужен, то его нужно уничтожить, чтобы освободить память для других объектов. Этим занимается специальный сборщик мусора (garbage collector). Но иногда программа написана таким образом, что сборщик мусора думает, что объект вам ещё нужен и не удаляет его из памяти. Тем самым кусок памяти остаётся занятым. А если процесс создания новых объектов с неправильным поведением повторяется неоднократно, то память просто забивается. В конце концов приложение может израсходовать лимит выделяемой памяти. Это состояние и называют утечкой памяти, т.е. приложению было выделено определённое количество памяти, а на самом деле используется меньшее количество. Откат, распил бюджета, коррупция. В этом случае приложение перестаёт работать, зависает и падает с ошибкой.
Данная статья является компиляцией из разных источников. Собрал в одном месте, чтобы немного упорядочить информацию.
Память условно имеет две области для хранения данных — стек (stack) и куча (heap).
Стек работает в порядке LIFO (Last In, First Out), то есть последний добавленный в стек фрагмент данных будет первым в очереди на вывод из стека. Каждый раз, когда функция объявляет новую переменную, переменная добавляется в стек, а когда эта переменная пропадает из области видимости, она автоматически удаляется из стека. Когда стековая переменная освобождается, эта область памяти становится доступной для других стековых переменных. Размер стека — это фиксированная величина, и превышение лимита выделенной на стеке памяти приведёт к переполнению стека. Размер задаётся при создании потока, и у каждой переменной есть максимальный размер, зависящий от типа данных.
Куча — это хранилище памяти, также расположенное в ОЗУ, которое допускает динамическое выделение памяти. Куча не имеет упорядоченного набора данных, это просто склад для ваших переменных. По завершении приложения все выделенные участки памяти освобождаются. Размер кучи задаётся при запуске приложения, но, в отличие от стека, он ограничен лишь физически, и это позволяет создавать динамические переменные.
Чтобы наглядно преддставить способ хранения объектов в памяти, напишем простую программу на Java.
Размещение в памяти при запуске выглядит следующим образом.
По рисунку видно, что в стек попали функция main(), переменная с примитивным типом int.
Также в стек попадает объект obj, когда он создаётся из класса Object, при этом в куче создаётся ссылка на класс (указатель).
Аналогично, в стеке появляется объект mem с ссылкой на класс в куче.
Для функции foo() в стеке создаётся отдельный блок. В этом блоке создаётся объект param с ссылкой в куче на класс Object и строковый объект с ссылкой в куче на отдельный блок String Pool.
Когда в программе выполнение доходит до закрывающей фигурной скобкой метода foo(), метод прекращает работу и объекты в стеке, относящиеся к блоку функции, освобождаются. Память выглядит следующим образом.
Последняя закрывающая фигурная скобка от функции main() закрывает эту функцию, освобождая свой блок данных.
Стек успешно очистился, когда все функции отработали. Но данные в куче ведут себя немного иначе. Они сами по себе не уходят. В Java имеется специальный помощник — сборщик мусора, который следит за порядком и если он заметит неиспользуемые объекты, то убирает их. Суть его работы состоит в том, чтобы смотреть, есть ли связь между данными в стеке и куче. Если у объекта нет ссылки на класс в куче, значит класс можно удалить из памяти. Идеальный порядок выглядит следующим образом.
Существуют различные методики обнаружения утечек памяти и способы борьбы с ней. Вам нужно научиться устранять типовые утечки памяти.
Сначала ответим на вопрос: а зачем исправлять эти ошибки, чем это грозит? Даже с утечками памяти приложение может работать.
Можно провести эксперимент, намеренно создав утечку памяти — активность при каждом повороте будет создавать новый экземпляр. На современном устройстве таким образом можно создать несколько десятков новых экранов, прежде чем приложение закроется с ошибкой. Но в среднем, пользователь открывает 3-5 экранов, поэтому вероятность появление ошибки маловероятно. Но данный пример не должен успокаивать вас. Не все телефоны выделяют много памяти приложению.
Основные проблемные источники: Context и его производные (Activity), внутренние классы (Inner Classes), анонимные классы (Anonymous Classes), Handlers c Runnable, Threads, TimerTask, SensorManager и другие менеджеры.
Самая главная рекомендация — никогда не сохраняйте ссылки на Context, Activity, View, Fragment, Service в статических переменных.
Например, хочется передать объект из одной активности в другой. Некоторые программисты создают статическую переменную для первой активности и обращаются к ней из второй. Это крайне неудачный подход. Не только потому, что он моментально приводит к утечке памяти (статическая переменная продолжит существовать пока существует приложение, и объект Activity, на который она ссылается, никогда не будет выгружен). Этот подход также может привести к ситуации, когда вы будете обмениваться информацией не с тем экраном, ведь экран, невидимый пользователю, может в любой момент быть уничтожен и создан заново, когда пользователь к нему вернётся.
Почему же утечка активности такая большая проблема? Дело в том, что если сборщик мусора не соберёт Activity, то он не соберёт и все View и Fragment, а вместе с ними и все прочие объекты, расположенные в Activity. В том числе не будут высвобождены картинки. Поэтому утечка любой активности — это, как правило, самая большая утечка памяти, которая может быть в вашем приложении.
Используйте передачу объектов через Intent, либо передавайте ID объекта (если у вас есть база данных, из которой этот id потом можно достать).
Этот пункт также относится к любым объектам, временем жизни которых напрямую или косвенно управляет Android, т.е. View, Fragment, Service и т.д.
Объекты View и Fragment содержат ссылку на Activity, в котором они расположены, поэтому, если утечёт один единственный View, утечёт сразу всё — Activity и все View в ней. И заодно все drawable и всё, на что у любого элемента из экрана есть ссылка!
Будьте аккуратны при передаче ссылки на Activity (View, Fragment, Service) в другие объекты.
Утечка через слушателей
Рассмотрим простой пример: ваше приложение для социальной сети отображает фамилию, имя и рейтинг текущего пользователя на каждом экране приложения. Объект с профилем текущего пользователя существует с момента входа в аккаунт до момента выхода из него, и все экраны вашего приложения обращаются за информацией к одному и тому же объекту. Этот объект также периодически обновляет данные с сервера, так как рейтинг может часто меняться. Необходимо, чтобы объект с профилем уведомлял текущую активность об обновлении рейтинга. Как этого добиться? Очень просто:
Как добиться в этой ситуации утечки памяти? Тоже очень несложно! Просто забудьте отписаться от уведомлений в методе onPause():
Из-за такой утечки памяти активность будет продолжать обновлять интерфейс каждый раз, когда профиль будет обновляться даже после того, как экран перестанет быть видим пользователю. Хуже того, таким образом экран может подписать 2, 3 или больше раза на одно и то же уведомление. Это может привести к видимым тормозам интерфейса в момент обновления профиля — и не только на этом экране.
Что делать, чтобы избежать этой ошибки? Во-первых, конечно нужно всегда внимательно следить за тем, что вы отписались от всех уведомлений в момент ухода Activity в фон.
Вы можете сохранять не ссылки на объекты, а слабые ссылки. Это особенно полезно для наследников класса View — ведь у них нет метода onPause() и не совсем понятно, в какой момент они должны отписываться от уведомления. Слабые ссылки не считаются сборщиком мусора как связи между объектами, поэтому объект, на который существуют только слабые ссылки, будет уничтожен, а ссылка перестанет ссылаться на объект и примет значение null.
Другой пример с использованием системных слушателей. Например, есть слушатель определения местоположения.
Если забудем снять регистрацию слушателя в onStop(), то пользователь может закрыть приложение, но сборщик мусора не сможет освободить память, так как LocationManager будет по-прежнему выполнять свою работу.
Пример утечки с внутренним классом
Часто в состав основного класса включают внутренний класс. Это нормально, но в некоторых случаях может стать причиной утечки памяти. Внутренний класс содержит ссылку на основной класс, но может иметь свой жизненный цикл.
Сам пример вполне нормальный. Но нужно помнить, что класс BackgroundTask хранит ссылку на активность. Если задача выполняется очень долго (плохое соединение, большая картинка на сервере), то сложная активность со всеми своими ресурсами остаётся в памяти, пока задача не будет отработана.
Есть разные варианты решения задачи. Часто рекомендуют подход с WeakReference.
Код усложнился, кроме того, вам придётся изучать устройство WeakReference.
Для примера на Kotlin можно убрать модификатор inner.
Если в качестве внутреннего класса использовать Handler, то студия будет выводить подсказку This Handler class should be static or leaks might occur (anonymous android.os.Handler). Код, чтобы увидеть подсказку.
Более подробное описание подсказки в студии:
Пример утечки с анонимным классом
Принцип утечки памяти схож с примером с внутренним классом, когда сохраняется ссылка на активность. Экземпляр анонимного класса живёт дольше, чем контейнер. Если анонимный класс вызывает какой-то метод, читает или записывает свойство в класс-контейнер, то он держит в памяти класс-контейнер.
Утечка через потоки
Случай первый — потоки. Создадим внутренний класс внутри активности. Внутренний класс будет иметь ссылку на активность.
В обычной ситуации пользователь запустит активность, запустится задача на двадцать секунд.
Когда задача выполнится, стек освободит объекты.
Затем сборщик мусора освободит объекты в куче.
Когда пользователь закроет активность, основной метод будет будет освобождён и активность также будет удалена из кучи. Мы возвращаемся к начальной позиции.
Рассмотрим случай, когда пользователь закроет активность или повернёт экран после десяти секунд.
Задача по-прежнему выполняется, ссылка на активность по-прежнему жива и мы имеем утечку памяти.
Когда метод run() выполнится, стек освободит объекты и сборщик мусора в порядке очереди почистит объекты в куче, так как они уже не будут иметь ссылок из стека.
После поворота устройства 5 раз мы можем наблюдать картину, как утекает память.
Singleton (Одиночка)
Когда происходит утечка памяти? Когда мы инициализируем синглтон в активности, то передаём ссылку на контекс-активность с долгим сроком жизни.
В этом случае ссылка на активность будет существовать пока не закроется приложение.
Чтобы избежать утечку, используйте контекст приложения, а не активности.
Или вы можете переписать класс одиночки.
Утечка с таймерами
Таймеры, которые не отменяются при выходе с экрана, тоже служат источниками утечки памяти.
Вы разработали приложение для социальной сети. В этом приложении можно обмениваться сообщениями между пользователями, и вы добавляете на экран обмена сообщениями таймер, который делает запрос на сервер каждые 10 секунд с целью получения новых сообщений, но вы забыли этот таймер выключить при выходе с экрана:
К сожалению, эту проблему сложно избежать. Единственные два совета, которые можно дать, будут такими же, как и в предыдущем пункте: будьте внимательны и периодически проверяйте приложение на утечки памяти. Вы также можете использовать аналогичный предыдущему пункту подход с использованием слабых ссылок.
Фрагменты
Никогда не сохраняйте ссылки на Fragment в активности или другом фрагменте.
Активность хранит ссылки на 5-6 запущенных фрагментов даже если на экране всегда виден только один. Один фрагмент хранит ссылку на другой фрагмент. Фрагменты, видимые на экране в разное время, общаются друг с другом по прямым закешированным ссылкам. FragmentManager в таких приложениях выполняет чаще всего рудиментарную роль — в нужный момент он подменяет содержимое контейнера нужным фрагментом, а сами фрагменты в back stack не добавляются (добавление фрагмента, на который у вас есть прямая ссылка, в back stack рано или поздно приведёт к тому, что фрагмент будет выгружен из памяти; после возврата к этому фрагменту будет создан новый, а ваша ссылка продолжит ссылаться на существующий, но невидимый пользователю фрагмент).
Это очень плохой подход по целому ряду причин. Во-первых, если вы храните в активности прямые ссылки на 5-6 фрагментов, то это тоже самое, как если бы вы хранили ссылки на 5-6 Activity. Весь интерфейс, все картинки и вся логика пяти неиспользуемых фрагментов не могут быть выгружены из памяти, пока запущено Activity.
Во-вторых, эти фрагменты становится крайне сложно переиспользовать. Попробуйте перенести фрагмент в другое место программы при условии, что он должен быть обязательно запущен в одном Activity с фрагментами, x, y и z, которые переносить не надо.
Относитесь к фрагментам как к Activity. Делайте их максимально модульными, общайтесь между фрагментами только через Activity и FragmentManager.
Рассмотренные примеры — это частные случаи одного общего правила. Все утечки памяти появляются тогда и только тогда, когда вы сохраняете ссылку на объект с коротким жизненным циклом (short-lived object) в объекте с длинным жизненным циклом (long-lived object).
OutOfMemoryError — частая причина падения приложения из-за нехватки памяти. Особенно, если вы активно используете изображения.
Утечки памяти, связанные с неправильным использованием android.os.Handler. Не совсем очевидно, но все, что вы помещаете в Handler, находится в памяти и не может быть очищено сборщиком мусора в течении некоторого времени. Иногда довольно длительного. Читайте статью Борьба с утечками памяти в Android. Часть 1
Пример утечки с системными менеджерами
В Android есть много системных менеджеров (содержат слово «Manager» в именах классов), которые следует регистрировать. И часто программисты забывают снять регистрацию.
Возьмём для примера класс LocationManager, который помогает определить местоположение. Напишем минимальный код.
Запустите пример. В студии внизу выберите вкладку 6: Android Monitor (Сейчас вместо него появился Profiler), а в ней вкладку Monitors. В верхней части окна будет блок Memory, который представляет для нас интерес.
Начинайте вращать устройство с запущенным приложением. Вы увидите, что ваше приложение начинает забирать память у устройства (тёмно-синий цвет).
Мне не удалось исчерпать всю память и сломать приложение, в какой-то момент умная система освобождала занятую память и всё повторялось снова. В других ситуациях может случиться так, что память кончится раньше, чем сообразит система.
Нажмите на третью кнопку в этом окне Dump Java Heap. Данное действие сгенерирует hprof-файл, содержащий слепок памяти в заданный момент. Далее студия автоматически откроет созданный файл, который можно изучить.
Обратите внимание на вкладку Analyzer Tasks сбоку в верхнем правом углу. Откройте эту вкладку. В ней вы увидите строчку с флажком Detect Leaked Activities (Обнаружить утекающие активности). В окнеAnalysis Results щёлкните по строке Leaked Activities, чтобы увидеть дополнительную информацию.
Видно, что при поворотах создавалось множество активностей MainActivity, а вместе с ней и объект LocationManager.
Добавим код в метод onDestroy(), как это предписано документацией.
Запустите приложение снова и начинайте вращать устройство. Сделайте дамп памяти для анализа. Вы увидите, что теперь активность не утекает. Могут остаться другие проблемы, влияющие на потребление памяти, но свою проблему мы решили. Поэтому не забывайте освобождать ресурсы, если об этом просят в документации.
В Android Studio есть специальный инструмент, который позволяет следить за памятью — Profiler, запускаемый из меню View | Tool Windows. Также имеется отдельный значок инструмента на панели в верхней части. Новый инструмент заменил Android Monitor в старых версиях студии.
Запустите профайлер, появится окно с четырьмя блоками: CPU, MEMORY, NETWORK, ENERGY. Нас интересует память. Щёлкаем в этой области, чтобы оставить слежение только за используемой памятью.
Нажмите кнопку Dump Java heap, чтобы получить дамп кучи. Рядом имеется кнопка очистки мусора Force garbage collection.
Источник