Android the following processes

Процессы и потоки в Android: пишем AsyncTask правильно

Продолжаю свои повествования об Android. И в этот раз хочу поделиться ценной информацией о процессах и потоках, которая должна быть хорошо усвоена и всегда оставаться под рукой во избежании ошибок и недопонимания при написании приложений. В конце статьи приведу пример реализации AsyncTask, который загружает в ImageView картинку по нажатию на кнопку.

Прежде всего отмечу, что подробнее о данной теме можно прочесть в данном руководстве — developer.android.com/guide/topics/fundamentals/processes-and-threads.html

На заметку о процессах и потоках в Android

Когда запускается компонент приложения и приложение не имеет других запущенных компонентов, Android создает новый процесс для приложения с одним потоком исполнения. По умолчанию все компоненты одного приложения запускаются в одном процессе, в потоке называемом «главный». Если компонент приложения запускается и уже существует процесс для данного приложения(какой-то компонент из приложения существует), тогда компонент запущен в этом процессе и использует его поток выполнения. Вы можете изменить данное поведение, задав разные процессы для разных компонентов вашего приложения. Кроме того вы можете добавить потоки в любой процесс.

Задать отдельный процесс для компонента можно с помощью файла манифеста. Каждый тег компонента(activity, service, receiver и provider) поддерживает атрибут android:process. Данный атрибут позволяет задать процесс, в котором будет выполняться компонент. Также вы можете задать процесс в котором будут выполняться компоненты разных приложений. Также данный атрибут поддерживается тегом application, что позволяет задать определенный процесс для всех компонентов приложения.

Android пытается поддерживать процесс приложения как можно дольше, но когда потребуются ресурсы старые процессы будут вытеснены по иерархии важности.

Существует 5 уровней иерархии важности: (процессы первого уровня из списка будут удалены последними)

1.Процесс с которым взаимодействует пользователь(Foreground process)
К таким процессам относится например: активити с которым взаимодействует пользовать; сервис(экземпляр Service), с которым взаимодействует пользователь; сервис запущенный методом startForeground(); сервис, который выполняет один из методов своего жизненного цикла; BroadcastReceiver который выполняет метод onReceive().

2.Видимый процесс
Процесс, в котором не выполнены условия из пункта №1, но который влияет на то, что пользователь видит на экране. К примеру, вызван метод onPause() активити.

3.Сервисный процесс
Служба запущенная методом startService()

4.Фоновый процесс
Процесс выполняемый в фоновом режиме, который невиден пользователю.

Отмечу, что в компонентах приложения существует метод onLowMemory(), но полагаться на то, что данный метод будет вызван нельзя, также как нельзя на 100% полагаться на метод onDestroy(), поэтому логику сохранения данных или каких-либо настроек можно осуществить в методе onStop(), который(как уверяют) точно вызывается.

Когда запускается приложение, система создает «главный» поток выполнения для данного приложения, который также называется UI-потоком. Этот поток очень важен, так как именно в нем происходит отрисовка виджетов(кнопочек, списков), обработка событий вашего приложения. Система не создает отдельный поток для каждого экземпляра компонента. Все компоненты, которые запущенны в одном процессе будут созданы в потоке UI. Библиотека пользовательского интерфейса Android не является потоково-безопасной, поэтому необходимо соблюдать два важных правила:

1) Не блокировать поток UI
2) Не обращаться к компонентам пользовательского интерфейса не из UI-потока

Теперь, предположим, что у нас возникла задача — загрузить картину в ImageView из сети и тут же ее отобразить. Как мы поступим? По логике: мы создадим отдельный поток, который и сделает всю нашу работу, примерно так:

Выглядит правдоподобно, так как мы вынесли операцию загрузки картинки в отдельный поток. Проблема в том, что мы нарушили правило №2. Исправить эту проблему можно с помощью следующих методов:

Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)

К примеру, воспользуемся первым из них:

Теперь реализация потоково-безопасная: сетевая операция выполняется в отдельном потоке, а к ImageView обращаемся из потока UI.
К счастью, данные операции можно объединить с помощью наследования класса Handler и реализации нужной логики, но лучшее решение — наследовать класс AsyncTask.

AsyncTask позволяет выполнить асинхронную работу и делать обновления пользовательского интерфейса.
Для обновления реализуйте метод onPostExecute(), а всю фоновую работу заключите в метод doInBackground(). После того, как вы реализуете свою собственную задачу, необходимо ее запустить методом execute().

Читайте также:  Com android phone xiaomi

Привожу обещанный пример AsyncTask, в котором реализована задача загрузки и отображения картинки(вариант с аннотациями и отклонением от применения стандартного протокола диалогов):

А теперь рассмотрим самый правильный вариант с точки зрения работы с диалогами:

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

Не забудьте добавить в свою разметку кнопочки атрибут с указанным значением: android:onClick=«runButtonHandler»

И добавлю: в оффициальном документе(Тыц ) также, как и в моем случае, не используется preExecute(), но если вам понадобится выполнить какие-то действия с вашим пользовательским интерфейсом до начала выполнения задачи, то смело используйте данный метод.

Параметры передаваемые в AsyncTask:
1. Параметры(в нашем случае адрес URL).
2. Прогресс (единицы задающие ход изменения задачи). В нашем случае не используется.
3. Результат выполнения задачи(в нашем случае объект Bitmap)

Код довольно прост: всю фоновую работу мы выполняем в методе doInBackGround(), вызывая метод publishProgress(), чтобы во время загрузки картинки крутился наш ProgressDialog при вызове метода onProgressUpdate(). После выполнения фоновой работы вызывается метод onPostExecute() в который передается результат работы метода doInBackGround(), в нашем случае это объект Bitmap и тут же мы его загружаем в ImageView.
Отмечу пару важных моментов, которые нужно учитывать:

1) Метод doInBackGround() выполняется в фоновом потоке, потому доступа к потоку UI внутри данного метода нет.
2) Методы onPostExecute() и onProgressUpdate() выполняются в потоке UI, потому мы можем смело обращаться к нашим компонентам UI.

Да, я снова применил библиотеку android-annotations, так что не пугайтесь аннотациям.

Хочу отметить важность понимания модели работы процессов в Android, чтобы не допустить ошибок при разработке приложений.

Данная статья это переработка доступной информации + личный опыт при реализации указанной задачи и работе с потоками.

Как всегда пожелания и замечания по поводу материала статьи в личку. Если вам есть что-то добавить или дополнить — смело пишите в комментарии.

UPD Статья обновленна добавлением более правильной версии в плане работы с диалогами.

Источник

Android Process: Я тебя породил, я тебя и …

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

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

Ни для кого не секрет, что процесс может быть убит системой. А вы интересовались, реально ли сымитировать это? Можно попробовать“натравить” систему на свое приложение, запуская кучу других приложений, съедающих знатный кусок памяти, а затем надеяться, что система таки снизошла до нас убив нужное приложение. Прямо какой-то шаманизм получается, а это стезя админов, но никак не программистов. Меня заинтересовало, как можно легко и быстро убить приложение, да так, будто это сделала система для освобождения ресурсов. Ведь если получится повторить подобное поведение в “лабораторных условиях”, можно будет отлавливать множество ошибок ещё на стадии разработки, либо с лёгкостью воспроизвести их для выявления причин.

Как оказалось, нужный механизм уже имеется в SDK, и это… барабанная дробь… Кнопка «Terminate Application».

Нажатие на нее аналогично выполнению следующего кода:
Это, собственно, и убивает процесс. Похожее действие происходит когда система пытается избавиться от ненужного процесса.

Для того, чтобы воспроизвести ситуацию с возобновлением работы приложения после его полной остановки, нужно проделать следующую последовательность действий:

  1. Используя Android Stidio запустить приложение;
  2. Свернуть приложение нажатием кнопки «Home»;
  3. В Android Studio, перейти на вкладку Android, выбрать приложение и нажать кнопку «Terminate Application»;
  4. Развернуть свернутое приложение.
Читайте также:  Очистка андроида от мусора samsung

Если Activity не совсем корректно обрабатывает восстановление после уничтожения, вы сразу это заметите. В лучшем случае оно упадёт, в худшем зависнет.

Итак, мы научились выманивать один из самых скрытных типов ошибок. Давайте научимся эти ошибки предвидеть. Начнем с самых очевидных случаев, а затем плавно перейдем к менее однозначным случаям.

Ситуация 1: Статик — это не надёжно

Как видно из кода, в первой Activity разработчик решил не “заморачиваться” и поместил данные в статические переменные. Это весьма соблазнительно, когда нужно передать достаточно неординарный объект, не поддающийся простой сериализации. Над второй Activity он тоже не долго думал и не сделал проверки на нул. Зачем, когда первая Activity всё уже сделала?

Проделываем описанную ранее последовательность по корректному уничтожению процесса на второй Activity. Приложение падает.

Что случилось?
Проблема тут не в отсутствии проверки на null. Самая страшная проблема — это потеря пользовательских данных. Статические объекты не должны быть хранилищем данных, особенно если это их единственное место. Касается это как обычных переменных так и синглтонов. Так что если в статике хранится что-то важное, будьте готовы это важное потерять в любой момент.

Что делать?
Наличие таких ошибок, зачастую, свидетельствует о низкой квалификации программиста, либо о слишком высоком чувстве лени. О том как делать правильно, написано огромное количество туториалов. В приведенном примере лучше всего подойдёт передача данных через бандл. Также можно писать эти данные в SharedPreferences, либо стоит задуматься о создании базы данных.
Важно: Не стоит забывать, что синглтон это тоже статик переменная. Если вы используете синглтон, то он должен выступать лишь как инструмент облегчающий доступ к данным, но ни как не быть единственным хранилищем для них.

Волшебная сила Application
Как часто я вижу советы использовать класс Application как синглтон либо инициализировать синглтон в методе onCreate() класса Application. Якобы после этого он станет круче чем Ленин, то есть будет живее всех живых при любых обстоятельствах. Возможно, это частный случай заблуждения встретившийся только мне. Причем, все публикации которые я находил, явно не заявляют о подобных свойствах синглтона. В некоторых из них говорится, что синглтон может быть уничтожен сборщиком мусора если инициировать его в классе Activity (что для меня звучит немного дико). В других пугают выгрузкой класса из класслоадера (а это уже похоже на правду).

Сейчас я не собираюсь выяснять, что тут правда а что домыслы. В любом случае это лишь снижает вероятность потери статик ссылки но ни как не спасает от остановки процесса. Остановка процесса приведет к полному уничтожению класслоадера, а вместе с ним и уничтожению всех классов, включая класс Application.

Ситуация 2: setRetainInstance как решение всех проблем

Такая реализация — стойка к любым вращениям экрана. Все будет работать как часы. До поры до времени…
Применим последовательность действий для хитрого уничтожения процесса, когда диалог открыт. При разворачивании ничего не произойдет. Однако, при вызове invokePersonChoose приложение вылетит с NullPointerException.

Что случилось?
setRetainInstance(true) не позволяло диалогу уничтожиться. После уничтожения процесса диалог все-таки был уничтожен. Activity восстанавливает фрагмент насколько это возможно. К сожалению, слушатель не восстанавливается, так как он был установлен совершенно в другом месте для совершенно другого объекта. Когда в диалоге, в методе invokePersonChoose, происходит обращение к слушателю, выбрасывается исключение. И беда тут не в отсутствии проверки на null. Поставить проверку на null без должной реакции на пустую ссылку будет еще более худшим решением.

Что делать?
В интернете описана куча способов передачи сообщений из фрагмента в Activity. К сожалению, не все из них правильные. Следующий способ правильный и один из моих любимых:

    Activity реализует нужный интерфейс:

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

Еще пара слов про setRetainInstance
Замечу, это лишь частный случай с setRetainInstance. Количество проблем которые он может скрыть(а не решить) немного больше. Вместе со слушателями также теряются и все остальные переменные. Все, что не было сохранено в методе onSaveInstanceState, будет потеряно.
Также, он скрывает проблему когда класс диалога анонимный. Допустим, в момент создания нового объекта диалога, переопределен какой либо метод, в этом случае создастся объект анонимного класса.

Читайте также:  Java works on android

Если этот диалог будет уничтожен и система попытается его восстановить выбросится исключение ClassNotFoundException.

Ситуация 3: Разорванные нити

Если запустить этот код и не дожидаясь его завершения свернуть приложение, а затем вырубить процесс, то при разворачивании ничего не предвещает беды, приложение не падает, а диалог показывается. Ждём немножко… Ещё ждём… Потом ещё ждём… Диалог не закрывается, хоть и должен был сделать это уже давно.

Что случилось?
При уничтожении процесса останавливаются все его потоки, а при восстановлении запускается только главный поток. Так что если ваш поток работает слишком долго, будьте готовы к тому, что он будет остановлен. Причём не обязательно делать что-то долго, на эти грабли можно наступить и при использовании wait notify. Особенно забавно будет, если в качестве объекта для блокировки использовать public static final Object, ведь мы то уже знаем, что статик объекты не исключение при уничтожении процесса.

Что делать?
Если задача занимает много времени вынести ее в отдельный сервис и запустить в новом процессе, плюс вывести foreground Notification, иначе процесс все равно будет убит. В случае с wait notify все немного сложнее и зависит от конкретной ситуации. Вообще, тема работы с потоками достаточно обширна, и давать какой-то конкретный совет тут неуместно. Разве что не усложнять и не лезть в дебри, из которых не сможешь вылезти.

Ситуация 4: Письмо в никуда

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

Во второй Activity есть кнопка, нажатие по которой шлет сообщение для смены цвета.

При возвращении на первую Activity цвет View будет изменен.

Однако, если перейти на вторую Activity, свернуть приложение, убить процесс, а затем развернуть приложение, случится кое-что не предвиденное. Сколько бы раз вы не нажимали на кнопку смены цвета, при возвращении на первую Activity ее View будет белоснежной.

Что случилось?
Не смотря на то, что стек активностей сохранился, после разворачивания приложения была восстановлена только вторая Activity. Фактически, сообщения о смене цвета уходили в никуда. Так как на тот момент не существовало объектов, подписанных на это событие. При возвращении на первую Activity она была восстановлена, но подписываться на событие смены цвета было уже поздно.

Что делать?
Первым делом нужно уяснить главное — если вы работаете с одной Activity, остальные для нее не существуют. Если нужно донести какую либо информацию, используйте предназначенный для этого setResult. Еще не следует слепо полагаться на жизненные циклы. Как видно из примера, если Activity 1 запустила Activity 2 то это еще не значит, что метод onCreate первой Activity был выполнен.
Также замечу, этот пример показывает не только на проблемы со стеком активностей, но и проблемы со всем, что связано с посылкой сообщений. Исключением будут только BroadcastReceiver-ы, прописанные в манифесте, либо запланированные через AlarmManager. Все остальное не дает 100% гарантии на доставку сообщения адресату.

Заключение

“Бывалым кодерам” эти ситуации могут показаться очевидными, а примеры на столько не естественными, что просто воротит. Однако, все мы когда-то начинали с нуля и писали код от которого сейчас было бы стыдно. Знай я об этих граблях в самом начале своего пути, шишек на лбу было бы меньше. Надеюсь, эта статья поможет наставить на путь истинный большое количество начинающих Android программистов. От “Бывалых” буду рад увидеть комментарии. А еще лучше если вы дополните список или напишете, помогла ли эта статья в ловле багов из разряда “Как?! Такого не может быть!”.

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

Источник

Оцените статью