Android Process: Я тебя породил, я тебя и …
Вы когда-нибудь задумывались о том, что происходит с вашим приложением после того, как система убила его процесс за ненадобностью? Печально, но многие об этом даже не беспокоятся, словно это будет происходить в параллельной вселенной и их это не касается. Особенно этому подвержены новички. Их слепая вера в непоколебимость статик ссылок просто поражает.
В этой статье я расскажу о некоторых ошибках, которые могут возникнуть в результате нарушения шестой заповеди (не убей) по отношению к процессу приложения, и о том как проверить на сколько качественно он возвращается с того света.
Ни для кого не секрет, что процесс может быть убит системой. А вы интересовались, реально ли сымитировать это? Можно попробовать“натравить” систему на свое приложение, запуская кучу других приложений, съедающих знатный кусок памяти, а затем надеяться, что система таки снизошла до нас убив нужное приложение. Прямо какой-то шаманизм получается, а это стезя админов, но никак не программистов. Меня заинтересовало, как можно легко и быстро убить приложение, да так, будто это сделала система для освобождения ресурсов. Ведь если получится повторить подобное поведение в “лабораторных условиях”, можно будет отлавливать множество ошибок ещё на стадии разработки, либо с лёгкостью воспроизвести их для выявления причин.
Как оказалось, нужный механизм уже имеется в SDK, и это… барабанная дробь… Кнопка «Terminate Application».
Нажатие на нее аналогично выполнению следующего кода:
Это, собственно, и убивает процесс. Похожее действие происходит когда система пытается избавиться от ненужного процесса.
Для того, чтобы воспроизвести ситуацию с возобновлением работы приложения после его полной остановки, нужно проделать следующую последовательность действий:
- Используя Android Stidio запустить приложение;
- Свернуть приложение нажатием кнопки «Home»;
- В Android Studio, перейти на вкладку Android, выбрать приложение и нажать кнопку «Terminate Application»;
- Развернуть свернутое приложение.
Если 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, будет потеряно.
Также, он скрывает проблему когда класс диалога анонимный. Допустим, в момент создания нового объекта диалога, переопределен какой либо метод, в этом случае создастся объект анонимного класса.
Если этот диалог будет уничтожен и система попытается его восстановить выбросится исключение 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 программистов. От “Бывалых” буду рад увидеть комментарии. А еще лучше если вы дополните список или напишете, помогла ли эта статья в ловле багов из разряда “Как?! Такого не может быть!”.
На этом закончу. Спасибо за внимание. Напоследок замечу, в рукаве у меня еще остался один неординарный случай. Он еще не изучен мной до конца, но я надеюсь в скором времени расколоть этот крепкий орешек. Так что ждите продолжения.
Источник
Как работает Android, часть 1
В этой серии статей я расскажу о внутреннем устройстве Android — о процессе загрузки, о содержимом файловой системы, о Binder и Android Runtime, о том, из чего состоят, как устанавливаются, запускаются, работают и взаимодействуют между собой приложения, об Android Framework, и о том, как в Android обеспечивается безопасность.
Немного фактов
Android — самая популярная операционная система и платформа для приложений, насчитывающая больше двух миллиардов активных пользователей. На ней работают совершенно разные устройства, от «интернета вещей» и умных часов до телевизоров, ноутбуков и автомобилей, но чаще всего Android используют на смартфонах и планшетах.
Android — свободный и открытый проект. Большинство исходного кода (который можно найти на https://source.android.com) распространяется под свободной лицензией Apache 2.0.
Компания Android Inc. была основана в 2003 году и в 2005 году куплена Google. Публичная бета Android вышла в 2007 году, а первая стабильная версия — в 2008, с тех пор мажорные релизы выходят примерно раз в год. Последняя на момент написания стабильная версия Android — 7.1.2 Nougat.
Android is Linux
По поводу такой формулировки было много споров, так что сразу поясню, что именно я имею в виду под этой фразой: Android основан на ядре Linux, но значительно отличается от большинства других Linux-систем.
Среди исходной команды разработчиков Android был Robert Love, один из самых известных разработчиков ядра Linux, да и сейчас компания Google остаётся одним из самых активных контрибьюторов в ядро, поэтому неудивительно, что Android построен на основе Linux.
Как и в других Linux-системах, ядро Linux обеспечивает такие низкоуровневые вещи, как управление памятью, защиту данных, поддержку мультипроцессности и многопоточности. Но — за несколькими исключениями — вы не найдёте в Android других привычных компонентов GNU/Linux-систем: здесь нет ничего от проекта GNU, не используется X.Org, ни даже systemd. Все эти компоненты заменены аналогами, более приспособленными для использования в условиях ограниченной памяти, низкой скорости процессора и минимального потребления энергии — таким образом, Android больше похож на встраиваемую (embedded) Linux-систему, чем на GNU/Linux.
Другая причина того, что в Android не используется софт от GNU — известная политика «no GPL in userspace»:
We are sometimes asked why Apache Software License 2.0 is the preferred license for Android. For userspace (that is, non-kernel) software, we do in fact prefer ASL 2.0 (and similar licenses like BSD, MIT, etc.) over other licenses such as LGPL.
Android is about freedom and choice. The purpose of Android is promote openness in the mobile world, and we don’t believe it’s possible to predict or dictate all the uses to which people will want to put our software. So, while we encourage everyone to make devices that are open and modifiable, we don’t believe it is our place to force them to do so. Using LGPL libraries would often force them to do just that.
Само ядро Linux в Android тоже немного модифицировано: было добавлено несколько небольших компонентов, в том числе ashmem (anonymous shared memory), Binder driver (часть большого и важного фреймворка Binder, о котором я расскажу ниже), wakelocks (управление спящим режимом) и low memory killer. Исходно они представляли собой патчи к ядру, но их код был довольно быстро добавлен назад в upstream-ядро. Тем не менее, вы не найдёте их в «обычном линуксе»: большинство других дистрибутивов отключают эти компоненты при сборке.
В качестве libc (стандартной библиотеки языка C) в Android используется не GNU C library (glibc), а собственная минималистичная реализация под названием bionic, оптимизированная для встраиваемых (embedded) систем — она значительно быстрее, меньше и менее требовательна к памяти, чем glibc, которая обросла множеством слоёв совместимости.
В Android есть оболочка командной строки (shell) и множество стандартных для Unix-подобных систем команд/программ. Во встраиваемых системах для этого обычно используется пакет Busybox, реализующий функциональность многих команд в одном исполняемом файле; в Android используется его аналог под названием Toybox. Как и в «обычных» дистрибутивах Linux (и в отличие от встраиваемых систем), основным способом взаимодействия с системой является графический интерфейс, а не командная строка. Тем не менее, «добраться» до командной строки очень просто — достаточно запустить приложение-эмулятор терминала. По умолчанию он обычно не установлен, но его легко, например, скачать из Play Store (Terminal Emulator for Android, Material Terminal, Termux). Во многих «продвинутых» дистрибутивах Android — таких, как LineageOS (бывший CyanogenMod) — эмулятор терминала предустановлен.
Второй вариант — подключиться к Android-устройству с компьютера через Android Debug Bridge (adb). Это очень похоже на подключение через SSH:
Из других знакомых компонентов в Android используются библиотека FreeType (для отображения текста), графические API OpenGL ES, EGL и Vulkan, а также легковесная СУБД SQLite.
Кроме того, раньше для реализации WebView использовался браузерный движок WebKit, но начиная с версии 7.0 вместо этого используется установленное приложение Chrome (или другое; список приложений, которым разрешено выступать в качестве WebView provider, конфигурируется на этапе компиляции системы). Внутри себя Chrome тоже использует основанный на WebKit движок Blink, но в отличие от системной библиотеки, Chrome обновляется через Play Store — таким образом, все приложения, использующие WebView, автоматически получают последние улучшения и исправления уязвимостей.
It’s all about apps
Как легко заметить, использование Android принципиально отличается от использования «обычного Linux» — вам не нужно открывать и закрывать приложения, вы просто переключаетесь между ними, как будто все приложения запущены всегда. Действительно, одна из уникальных особенностей Android — в том, что приложения не контролируют напрямую процесс, в котором они запущены. Давайте поговорим об этом подробнее.
Основная единица в Unix-подобных системах — процесс. И низкоуровневые системные сервисы, и отдельные команды в shell’е, и графические приложения — это процессы. В большинстве случаев процесс представляет собой чёрный ящик для остальной системы — другие компоненты системы не знают и не заботятся о его состоянии. Процесс начинает выполняться с вызова функции main() (на самом деле _start ), и дальше реализует какую-то свою логику, взаимодействуя с остальной системой через системные вызовы и простейшее межпроцессное общение (IPC).
Поскольку Android тоже Unix-подобен, всё это верно и для него, но в то время как низкоуровневые части — на уровне Unix — оперируют понятием процесса, на более высоком уровне — уровне Android Framework — основной единицей является приложение. Приложение — не чёрный ящик: оно состоит из отдельных компонентов, хорошо известных остальной системе.
У приложений Android нет функции main() , нет одной точки входа. Вообще, Android максимально абстрагирует понятие приложение запущено как от пользователя, так и от разработчика. Конечно, процесс приложения нужно запускать и останавливать, но Android делает это автоматически (подробнее я расскажу об этом в следующих статьях). Разработчику предлагается реализовать несколько отдельных компонентов, каждый из которых обладает своим собственным жизненным циклом.
In Android, however, we explicitly decided we were not going to have a main() function, because we needed to give the platform more control over how an app runs. In particular, we wanted to build a system where the user never needed to think about starting and stopping apps, but rather the system took care of this for them… so the system had to have some more information about what is going on inside of each app, and be able to launch apps in various well-defined ways whenever it is needed even if it currently isn’t running.
Для реализации такой системы нужно, чтобы приложения имели возможность общатся друг с другом и с системными сервисами — другими словами, нужен очень продвинутый и быстрый механизм IPC.
Этот механизм — Binder.
Binder
Binder — это платформа для быстрого, удобного и объектно-ориентированного межпроцессного взаимодействия.
Разработка Binder началась в Be Inc. (для BeOS), затем он был портирован на Linux и открыт. Основной разработчик Binder, Dianne Hackborn, была и остаётся одним из основных разработчиков Android. За время разработки Android Binder был полностью переписан.
Binder работает не поверх System V IPC (которое даже не поддерживается в bionic), а использует свой небольшой модуль ядра, взаимодействие с которым из userspace происходит через системные вызовы (в основном ioctl ) на «виртуальном устройстве» /dev/binder . Со стороны userspace низкоуровневая работа с Binder, в том числе взаимодействие с /dev/binder и marshalling/unmarshalling данных, реализована в библиотеке libbinder.
Низкоуровневые части Binder оперируют в терминах объектов, которые могут пересылаться между процессами. При этом используется подсчёт ссылок (reference-counting) для автоматического освобождения неиспользуемых общих ресурсов и уведомление о завершении удалённого процесса (link-to-death) для освобождения ресурсов внутри процесса.
Высокоуровневые части Binder работают в терминах интерфейсов, сервисов и прокси-объектов. Описание интерфейса, предоставляемого программой другим программам, записывается на специальном языке AIDL (Android Interface Definition Language), внешне очень похожем на объявление интерфейсов в Java. По этому описанию автоматически генерируется настоящий Java-интерфейс, который потом может использоваться и клиентами, и самим сервисом. Кроме того, по .aidl -файлу автоматически генерируются два специальных класса: Proxy (для использования со стороны клиента) и Stub (со стороны сервиса), реализующие этот интерфейс.
Для Java-кода в процессе-клиенте прокси-объект выглядит как обычный Java-объект, который реализует наш интерфейс, и этот код может просто вызывать его методы. При этом сгенерированная реализация прокси-объекта автоматически сериализует переданные аргументы, общается с процессом-сервисом через libbinder, десериализует переданный назад результат вызова и возвращает его из Java-метода.
Stub работает наоборот: он принимает входящие вызовы через libbinder, десериализует аргументы, вызывает абстрактную реализацию метода, сериализует возвращаемое значение и передаёт его процессу-клиенту. Соответственно, для реализации сервиса программисту достаточно реализовать абстрактные методы в унаследованном от Stub классе.
Такая реализация Binder на уровне Java позволяет большинству кода использовать прокси-объект, вообще не задумываясь о том, что его функциональность реализована в другом процессе. Для обеспечения полной прозрачности Binder поддерживает вложенные и рекурсивные межпроцессные вызовы. Более того, использование Binder со стороны клиента выглядит совершенно одинаково, независимо от того, расположена ли реализация используемого сервиса в том же или в отдельном процессе.
Для того, чтобы разные процессы могли «найти» сервисы друг друга, в Android есть специальный сервис ServiceManager, который хранит, регистрирует и выдаёт токены всех остальных сервисов.
Binder широко используется в Android для реализации системных сервисов (например, пакетного менеджера и буфера обмена), но детали этого скрыты от разработчика приложений высокоуровневыми классами в Android Framework, такими как Activity, Intent и Context. Приложения могут также использовать Binder для предоставления друг другу собственных сервисов — например, приложение Google Play Services вообще не имеет собственного графического интерфейса для пользователя, но предоставляет разработчикам других приложений возможность пользоваться сервисами Google Play.
Подробнее про Binder можно узнать по этим ссылкам:
В следующей статье я расскажу о некоторых идеях, на которых построены высокоуровневые части Android, о нескольких его предшественниках и о базовых механизмах обеспечения безопасности.
Источник