Сейчас давайте подробнее рассмотрим жизненный цикл приложений Android. Он немного отличается от жизненного цикла приложений в большинстве систем.
На обычном рабочем столе Linux или Windows вы запускаете множество приложений и просматриваете результаты их работы в отдельных окнах. Одно из рабочих окон «активировано», то есть владеет фокусом ввода, однако все программы равноправны между собой. По своему желанию вы можете переключаться между ними и перемещать их, для того чтобы видеть свои действия и закрывать программы, которые не нужны.
Android работает не так.
В Android есть приложение переднего плана, активное приложение, которое обычно занимает весь экран, кроме строки состояния. Когда пользователь включает свой телефон, первое приложение, которое он видит, — это программа Home (Домашний экран).
Когда пользователь запускает программу, Android начинает ее исполнение и делает ее активной. Из этого приложения пользователь может вызвать другое приложение или другой экран в том же самом приложении, и так далее. Все эти программы и экраны записываются в стек приложений (application stack) системным Менеджером деятельностей. В любое время пользователь может нажать кнопку Back и вернуться к предыдущему экрану в стеке. С точки зрения пользователя, это работает почти так же, как перемещение по истории просмотров в веб-браузере, где нажатие кнопки Back возвращает его на предыдущую страницу.
Процесс != Приложение
Изнутри каждый экран пользовательского интерфейса представлен классом Activity. Каждая деятельность имеет собственный жизненный цикл. Приложение — это одна или несколько деятельностей плюс процесс Linux, содержащий их. Это звучит весьма просто, не так ли? Но не обольщайтесь; я послал вам крученый мяч.
В Android приложения могут быть «живыми» даже в том случае, если процессы «убиты». Говоря другим языком, цикл жизненной активности приложения не привязан к жизненному циклу процессов. Процессы — это одноразовые контейнеры для деятельностей. Возможно, это отличается от всего, к чему вы привыкли в других системах, поэтому давайте посмотрим на процессы поближе, прежде чем продолжить.
Жизненные циклы богатых и знаменитых
Приложения для платформы Android не имеют метода main. Соответственно, нужно понимать, как они начинаются, как их остановить или как они останавливаются сами.
Класс Activity содержит ряд четко определенных методов жизненного цикла, которые вызываются, когда приложение запускается, приостанавливается, перезапускается и т.д., а также метод, который вы можете вызвать, чтобы отметить активность как законченную.
Приложение для платформы Android работает в своем собственном процессе Unix/Linux, поэтому в целом оно не может напрямую влиять на какое-либо другое запущенное приложение. Интерфейсы среды выполнения Android Runtime взаимодействуют с операционной системой, чтобы сообщать вам, когда ваше приложение запускается, когда пользовать переключается на другое приложение и т.д. Для приложений Android существует четко определенный жизненный цикл.
Приложение Android может находится в одном из трех состояний:
Активно, когда приложение видимо для пользователя и работает.
Приостановлено, когда приложение частично затенено и потеряло фокус ввода (например, когда диалоговое окно перекрывает вашу активность).
Остановлено, когда приложение полностью скрыто от просмотра.
В течение жизни каждая деятельность программы Android может находиться в одном из нескольких состояний, как показано на рис. 2.3. Вы, разработчик, не контролируете состояние программы. Всем этим управляет система. Однако вы получаете уведомления, когда состояние изменяется, благодаря вызовам метода onXX().
Вы переопределяете эти методы в классе Activity, и Android вызывает их в соответствующее время:
• onCreate(Bundle): этот метод вызывается при первом запуске деятельности. Используйте его для начальной инициализации, например для создания пользовательского интерфейса. onCreate() имеет один параметр, который может быть либо установлен в null, либо содержать информацию о состоянии, ранее сохраненную методом onSaveInstanceState(). Здесь обычно выполняется работа конструктор: настройка главного окна, добавление слушателей кнопок для выполнения работы (включая запуск дополнительной активности) и т.д. Это тот самый метод, которого требует даже простейшая активность платформы.
Рис. 2.3. Жизненный цикл деятельностей Android
• onStart(): этот метод указывает на то, что приложение готово для показа пользователю.
• onResume(): этот метод вызывается, когда деятельность может начать взаимодействие с пользователем. Это неплохое место кода для запуска анимации или музыки.
• onPause(): этот метод запускается, когда деятельность готова перейти в фоновый режим, обычно по причине того, что другая деятельность была запущена перед ней. Это место кода, где вам следует выполнить сохранение состояния программы, такого как, например, измененные записи базы данных.
• onStop(): вызывается, когда деятельность больше не видна пользователю и на некоторое время не востребована. Если в системе мало памяти, onStop() может быть никогда не вызван (система может просто завершить ваш процесс).
• onRestart(): вызов этого метода указывает на то, что ваша деятельность показана пользователю и вышла из состояния остановки.
• onDestroy(): вызывается непосредственно перед тем, как деятельность будет уничтожена. Если в системе мало памяти, onDestroy() может быть никогда не вызван (система может просто завершить процесс).
• onSaveInstanceState(Bundle): Android вызывает этот метод для того, чтобы разрешить деятельности сохранить свое предыдущее состояние, такое как позиция курсора в текстовом поле. Обычно не требуется переопределять его, так как стандартный вариант метода сохраняет состояние всех пользовательских элементов управления автоматически.
• onRestoreInstanceState(Bundle): этот метод вызывается, когда деятельность повторно инициализирована из состояния, ранее сохраненного методом onSaveInstanceState(). Стандартная реализация метода восстанавливает состояние пользовательского интерфейса.
Неактивные деятельности могут быть остановлены, или процессы Linux, которые их обслуживают, могут быть уничтожены в любое время, для того чтобы освободить ресурсы для новых деятельностей. Это вполне обычное дело, поэтому очень важно, чтобы ваше приложение разрабатывалось с самого начала с учетом возможности такого события. В некоторых случаях вызов метода onPause() может быть последним методом, вызванным в деятельности, поэтому именно здесь нужно сохранить данные, которые вы не хотите потерять до следующего запуска приложения.
В дополнение к управлению жизненным циклом приложения фреймворк Android предоставляет множество строительных блоков, которые могут быть использованы при создании вашего приложения. Давайте взглянем на них.
Строительные блоки
В Android SDK определено несколько объектов, с которыми должен быть хорошо знаком каждый разработчик. Наиболее важные из них — это деятельности (activities), намерения (intents), сервисы(services) и контент-провайдеры (content providers). Позже вы познакомитесь с примерами их использования, а сейчас мне хотелось бы кратко их представить.
Деятельности (Активности)
Деятельность — это окно или экран пользовательского интерфейса. Приложение может определить одну или несколько деятельностей для поддержки различных стадий работы программы. Как говорилось в разделе 2.2 «Оно живое!», каждая деятельность ответственна за сохранение своего состояния, которое может быть восстановлено позднее как часть жизненного цикла приложения (см. раздел 3.3 «Создание стартового экрана», в котором вы найдете соответствующий пример). Деятельности расширяют класс Context, поэтому вы можете использовать их для получения глобальной информации о приложении.
Намерения
Намерение — это механизм для описания одного действия, такого как «выбрать фотографию», «позвонить домой» или «открыть двери модуля». В Android почти все работает благодаря намерениям, и существует множество возможностей по замене или повторному использованию компонентов.
Допустим, есть намерение «отправить электронную почту». Если ваше приложение должно отправить почту, активируйте это намерение. Или, при написании нового приложения для работы с электронной почтой, зарегистрируйте деятельность для обработки этого намерения и замените предустановленную программу электронной почты. Когда пользователь попытается отправить электронную почту, ваша программа будет вызвана вместо стандартной.
Код не создает объекты с помощью оператора new, как в обычной программе, а запрашивает обращение к активности, службе и т.д., используя намерение (intent), который указывает на ваше желание что-то сделать. Намерение может запускать активность в приложении (по имени класса), запускать активность в другом приложении (путем указания типа контента и другой информации), запускать службы и запрашивать другие операции.
Сервисы
Сервис — это задача, которая выполняется в фоновом режиме без прямого взаимодействия с пользователем. Сервисы похожи на демоны Unix. Например, рассмотрим музыкальный проигрыватель. Проигрывание музыки может быть запущено по намерению, но вы хотите, чтобы она играла даже тогда, когда пользователь переместился в другую программу. Для этого код, который выполняет проигрывание музыки, должен находиться внутри сервиса. Позже другая деятельность может подключиться к этому сервису и сообщить ему, что следует переключить или остановить воспроизведение. Android поставляется с множеством встроенных сервисов вместе с удобными API для доступа к ним.
Сервисы (Services). Сервис - компонент, который работает в фоновом режиме, выполняет длительные по времени операции или работу для удаленных процессов. Сервис не предоставляет пользовательского интерфейса. Например, сервис может проигрывать музыку в фоновом режиме, пока пользователь использует другое приложение, может загружать данные из сети, не блокируя взаимодействие пользователя с активностью. Сервис может быть запущен другим компонентом и после этого работать самостоятельно, а может остаться связанным с этим компонентом и взаимодействовать с ним.
Существует два способа существования сервисов:
• первый заключается в том, что сервис запущен (started) и работает самостоятельно в фоновом режиме, так он может работать неопределенно долго, пока не выполнит свою задачу;
• второй заключается в том, что сервис привязан (bound) к некоторому компоненту или нескольким компонентам, в этом случае сервис предлагает интерфейс для взаимодействия с компонентом и работает пока привязан хотя бы к одному компоненту, как только связь со всеми компонентами разрывается сервис завершает свою работу.
Для создания сервиса необходимо создать класс-наследник класса Service напрямую или через любого его потомка. При этом в реализации класса необходимо переопределить (т. е. написать свою реализацию) некоторые методы, управляющие ключевыми аспектами жизненного цикла сервиса и обеспечивающие механизм связывания компонентов с сервисом, в соответствующем случае. Рассмотрим наиболее важные методы требующие реализации при создании сервиса.
onStartCommand() - метод, вызываемый системой, когда некоторый компонент, например активность, вызывает метод startService(). В этом случае сервис запускается и может работать в фоновом режиме неопределенно долго, поэтому необходимо позаботиться об остановке сервиса, когда он выполнит свою работу. Для остановки сервиса используется метод stopSelf() в случае, когда сервис сам прекращает свою работу, или stopService() в случае, когда работу сервиса прекращает некоторый компонент. Нет необходимости писать реализацию метода onStartCommand(), если не предполагается самостоятельной работы сервиса (т. е. он будет работать только в связке с некоторыми компонентами).
onBind() - метод, вызываемый системой, когда некоторый компонент желает привязать к себе сервис и вызывает метод bindService(). Этот метод должен возвращать реализацию интерфейса IBinder, которая может быть использована компонентом-клиентом для взаимодействия с сервисом. Метод onBind() необходимо реализовать в любом случае, но, если не предполагается связывания сервиса с какими-либо компонентами, возвращаемое значение должно быть равным null.
Необходимо отметить, что сервис может быть запущен как самостоятельная единица, а в последствии может быть привязан к некоторым компонентам. В этом случае в сервисе должны быть обязательно реализованы оба метода onStartCommand() и onBind().
onCreate() - метод, вызываемый системой, при первом обращении к сервису для выполнения первоначальных настроек. Этот метод вызывается до вызова методов onStartCommand() и/или onBind().
onDestroy() - метод, вызываемый системой, когда сервис либо выполнил все действия, для которых создавался, либо больше не связан ни с одним компонентом, т. е. его услуги больше не требуются. В реализации этого метода необходимо предусмотреть освобождение всех ресурсов, таких как потоки, зарегистрированные слушатели, приемники и т. д. Вызов этого метода является последними вызовом, который может получить сервис.
На рис. 5 показан жизненный цикл сервиса, левая диаграмма показывает жизненный цикл самостоятельного сервиса, правая - жизненный цикл сервиса, привязанного к некоторым компонентам. На рисунке хорошо видно, что жизненный цикл сервиса намного проще жизненного цикла активности. Однако для разработчика понимание того, как именно сервис создается, запускается и завершает свою работу, может оказаться даже более важным, т. к. сервис работает в фоновом режиме и пользователь может и не осознавать, что в некоторых случаях он имеет дело с работой сервисов.
Android принудительно останавливает работу сервисов только, когда ресурсов системы не хватает для активности, которая работает в данный момент на переднем плане. Приоритет работающих сервисов всегда выше, чем у приостановленных или полностью невидимых активностей, а если сервис привязан к выполняющейся активности, то его приоритет еще выше. С другой стороны, со временем приоритет самостоятельно работающего сервиса понижается и его шансы быть принудительно остановленным системой в случае нехватки ресурсов повышаются. В связи с этим имеет смысл проектировать сервис таким образом, чтобы через некоторое время он требовал у системы перезапуска. В случае если система все-таки экстренно завершила работу сервиса, она перезапустит его как только освободятся ресурсы.
Контент-провайдеры
Контент-провайдер — это набор данных, «завернутый» в пользовательский интерфейс API для чтения и записи. Это лучший способ разделять глобальные данные между приложениями. Например, Google предоставляет контент-провайдер для адресной книги. Вся информация здесь — имена, адреса, номера телефонов и так далее — может быть использована любыми приложениями, которым она нужна.
Контент-провайдеры (Content providers). Контент-провайдер управляет распределенным множеством данных приложения. Данные могут храниться в файловой системе, в базе данных SQLite, в сети, в любом другом доступном для приложения месте. Контент-провайдер позволяет другим приложениям при наличии у них соответствующих прав делать запросы или даже менять данные. Например, в системе Android есть контент-провайдер, который управляет информацией о контактах пользователя. В связи с этим, любое приложение с соответствующими правами может сделать запрос на чтение и запись информации какого-либо контакта. Контент-провайдер может быть также полезен для чтения и записи приватных данных приложения, не предназначенных для доступа извне.
Для реализации провайдера в Android приложении должен быть создан набор классов в соответствии с манифестом приложения. Один из этих классов должен быть наследником класса ContentProvider, который обеспечивает интерфейс между контент-провайдером и другими приложениями. Основное назначение этого компонента приложения заключается в предоставлении другим приложениям доступа к данным, однако ничто не мешает в приложении иметь активность, которая позволит пользователю запрашивать и изменять данные, находящиеся под управлением контент-провайдера.
В мобильных приложениях контент-провайдеры необходимы в следующих случаях:
• приложение предоставляет сложные данные или файлы другим приложениям;
• приложение позволяет пользователям копировать сложные данные в другие приложения;
• приложение предоставляет специальные варианты поиска, используя поисковую платформу (framework).
Если приложение требует использования контент-провайдера, необходимо выполнить несколько этапов для создания этого компонента:
Проектирование способа хранения данных. Данные, с которыми работают контент-провайдеры, могут быть организованы двумя способами:
• Данные представлены файлом, например, фотографии, аудио или видео. В этом случае необходимо хранить данные в собственной области памяти приложения. В ответ на запрос от другого приложения, провайдер может возвращать ссылку на файл
• Данные представлены некоторой структурой, например, таблица, массив. В этом случае необходимо хранить данные в табличной форме. Строка таблицы представляет собой некоторую сущность, например, сотрудник или товар. А столбец - некоторое свойство этой сущности, например, имя сотрудника или цена товара. В системе Android общий способ хранения подобных данных - база данных SQLite, но можно использовать любой способ постоянного хранения.
Создание класса-наследника от класса ContentProvider напрямую или через любого его потомка. При этом в реализации класса необходимо переопределить (т. е. написать свою реализацию) обязательные методы.
query() - метод, извлекающий данные из провайдера, в качестве аргументов получает таблицу, строки и столбцы, а также порядок сортировки результата, возвращает объект типа Cursor.
insert() - метод, добавляющий новую строку, в качестве аргументов получает таблицу, и значения элементов строки, возвращает URI добавленной строки.
update() - метод, обновляющий существующие строки, в качестве аргументов получает таблицу, строки для обновления и новые значения элементов строк, возвращает количество обновленных строк.
delete() - метод, удаляющий строки, в качестве аргументов принимает таблицу и строки для удаления, возвращает количество удаленных строк.
getType() - метод, возвращающий String в формате MIME, который описывает тип данных, соответствующий URI.
onCreate() - метод, вызываемый системой, сразу после создания провайдера, включает инициализацию провайдера. Стоит отметить, что провайдер не создается до тех пор, пока объект ContentResolver не попытается получить к нему доступ
Созданный контент-провайдер управляет доступом к структурированным данным, выполняя обработку запросов от других приложений. Все запросы, в конечном итоге, вызывают объект ContentResolver, который в свою очередь вызывает подходящий метод объекта ContentProvider для получения доступа. Все вышеперечисленные методы, кроме onCreate(), вызываются приложениемклиентом. И все эти методы имеют такую же сигнатуру, как одноименные методы класса ContentResolver.
Определение строки авторизации провайдера, URI для его строк и имен столбцов. Если от провайдера требуется управление намерениями, необходимо определить действия намерений, внешние данные и флаги. Также необходимо определить разрешения, которые необходимы приложениям для доступа к данным провайдера. Все эти значения необходимо определить как константы в отдельном классе, этот класс в последствии можно предоставить другим разработчикам.
Приемники широковещательных сообщений (Broadcast Receivers).
Приемники широковещательных сообщений (Broadcast Receivers). Приемник - компонент, который реагирует на широковещательные извещения. Большинство таких извещений порождаются системой, например, извещение о том, что экран отключился или низкий заряд батареи. Приложения также могут инициировать широковещание, например, разослать другим приложениям сообщение о том, что некоторые данные загружены и доступны для использования. Хотя приемники не отображают пользовательского интерфейса, они могут создавать уведомление на панели состояний, чтобы предупредить пользователя о появлении сообщения. Такой приемник служит проводником к другим компонентам и предназначен для выполнения небольшого объема работ, например, он может запустить соответствующий событию сервис.
Каждый широковещательный приемник является наследником класса BroadcastReceiver. Этот класс рассчитан на получение объектов-намерений отправленных методом sendBroadcast().
Можно выделить две разновидности широковещательных сообщений:
• Нормальные широковещательные сообщения передаются с помощью Context.sendBroadcast в асинхронном режиме. Все приемники срабатывают в неопределенном порядке, часто в одно и то же время.
• Направленные широковещательные сообщения передаются с помощью Context.sendOrderedBroadcast только одному приемнику в один момент времени. Как только приемник сработает, он может передать сообщение следующему приемнику, а может прервать вещание так, что больше ни один приемник это сообщение не получит.
Даже в случае нормального широковещания могут сложиться ситуации, в которых система будет передавать сообщения только одному приемнику в один момент времени. Особенно это актуально для приемников, которые требуют создания процессов, чтобы не перегружать систему новыми процессами. Однако в этом случае ни один приемник не может прервать широковещание. Объект типа BroadcastReceiver действителен только во время вызова метода onRecieve(), как только метод выполнен, система завершает работу объекта и больше не активирует его.
Использование ресурсов
При разработке мобильных приложений необходимо выработать привычку отделять ресурсы приложения от кода. К ресурсам приложения могут относиться: изображения, строки, цвета, компоновки элементов пользовательского интерфейса (layout) и т. д. Отделение ресурсов от кода позволяет использовать альтернативные ресурсы для различных конфигураций устройств: язык, разрешение экрана и т. д. Для обеспечения совместимости с различными конфигурациями, ресурсы необходимо сгруппировать в директории по типу ресурсов и конфигурации устройства, полученные директории поместить в папку res/.
Ресурс — это локализованная текстовая строка, изображение или другой небольшой объем информации, в котором нуждается программа, не являющийся программным кодом. При сборке все ресурсы встраиваются в программу. Ресурсы используются для локализации продуктов или для поддержки устройств различных типов (см. раздел 3.4 «Использование альтернативных ресурсов».
Вы можете создавать и хранить ресурсы в папке res внутри проекта. Компилятор ресурсов Android обрабатывает ресурсы в соответствии с именем подпапки, в которой они расположены, и форматом файла. Например, графические файлы формата PNG и JPG должны находиться в папке, название которой начинается с res/drawable, а XML-файлы, которые описывают варианты компоновки экрана, должны располагаться в папке, имя которой начинается с res/layout. Добавляйте суффиксы для отдельных языков, ориентации экрана, плотности пикселей и так далее (см. раздел 13.5 «Все экраны, большие и маленькие»).
Компилятор ресурсов сжимает и упаковывает ресурсы и затем создает класс, называющийся R, который содержит идентификаторы, использованные при обращении к этим ресурсам в программе. Такой подход немного отличается от использования стандартных ресурсов Java, обращаться к которым можно по ключевым строкам. Выполнение операций с ресурсами подобным образом позволяет Android быть уверенным в том, что все ваши ссылки верны, и экономит пространство, не храня все ключи ресурсов.
Для любого типа ресурсов можно определить две группы. Первая определяет ресурсы, которые будут использоваться независимо от конфигурации устройства или в том случае, когда под конфигурацию нет подходящих альтернативных ресурсов. Эта группа называется ресурсы по умолчанию (default). Вторая группа определяет ресурсы, подходящие для определенной конфигурации устройства, размещается в директории с названием, обозначающим данную конфигурацию. Такие ресурсы называются альтернативными.
Следует отметить, что файлы ресурсов нельзя размещать в папку res/ напрямую, они обязательно должны размещаться в соответствующем каталоге, иначе будет выдана ошибка компиляции.
Все ресурсы, которые содержатся в рассмотренных поддиректориях являются ресурсами по умолчанию. Понятно, что различные типы устройств могут требовать различных типов ресурсов. Например, для устройств с разными размерами экрана компоновки элементов пользовательского интерфейса должны отличаться.
Манифест приложения
Корневой каталог каждого приложения под Android должен содержать файл AndroidManifest.xml (в точности с таким названием). Манифест приложения содержит всю необходимую информацию, используемую системой для запуска и выполнения приложения. Основная информация, содержащаяся в манифесте:
• Имя Java пакета приложения, которое используется как уникальный идентификатор приложения.
• Описание компонентов приложения: активностей, сервисов, приемников широковещательных сообщений и контент-провайдеров, которые составляют приложение. Для каждого компонента приложения определено имя соответствующего класса и объявлены их основные свойства (например, с какими сообщениями-намерениями они могут работать). Эта информация позволяет системе Android узнать какие компоненты и при каких условиях могут быть запущены.
• Определение процессов, в которых будут выполняться компоненты приложения.
• Объявление полномочий, которыми должно обладать приложение для доступа к защищенным частям API и взаимодействия с другими приложениями.
• Объявление полномочий, которыми должны обладать другие приложения для взаимодействия с компонентами данного.
• Список вспомогательных классов, которые предоставляют информацию о ходе выполнения приложения. Эти объявления содержатся в манифесте пока идет разработка и отладка приложения, перед публикацией приложения они удаляются.
• Определение минимального уровня Android API для приложения.
• Список библиотек связанных с приложением
В файле манифеста только два элемента: и являются обязательными и при этом встречаются ровно по одному разу. Остальные элементы могут встречаться несколько раз или не появляться совсем, в этом случае манифест определяет пустое приложение.
Следующий листинг демонстрирует общую структуру файла манифеста:
В манифесте элементы одного уровня, такие как activity,service ,receiver , provider, могут следовать друг за другом в любой последовательности. Элемент является исключением из этого правила, он должен следовать за соответствующей активностью.
Безопасность и защищенность
Как упоминалось ранее, каждое приложение работает в собственном процессе Linux. Аппаратное обеспечение не позволяет одному процессу получать доступ к памяти другого процесса. Более того, каждому приложению присваивается индивидуальный идентификатор. Любые файлы, созданные им, не могут быть прочитаны или переписаны другим приложением.
Кроме того, доступ к определенным критическим операциям ограничен, и следует особым образом запрашивать разрешение на их использование в файле, который называется AndroidManifest.xml. Когда приложение устанавливается, менеджер пакетов либо предоставляет, либо не предоставляет разрешения, основываясь на сертификатах и, если нужно, на ответах пользователя. Вот некоторые из наиболее распространенных разрешений, которые вам понадобятся:
• INTERNET: доступ к Интернету.
• READ_CONTACTS: чтение (но не запись) данных адресной книги пользователя.
• WRITE_CONTACTS: запись (но не чтение) в адресную книгу пользователя.
• RECEIVE_SMS: отслеживание входящих SMS-сообщений (текстовых).
• ACCESS_COARSE_LOCATION: использование средств грубого определения местоположения, например вышек сотовой связи или точек доступа Wi-Fi.
• ACCESS_FINE_LOCATION: использование более точных средств определения местоположения, таких как GPS.
Например, для отслеживания входящих SMS-сообщений добавьте следующие строки в файл манифеста:
package="com.google.android.app.myapp"
Android может также запрещать доступ к целым частям системы. Используя теги XML в AndroidManifest.xml, вы можете ограничить круг пользователей, которым разрешено запускать деятельность, запускать сервис или подключаться к нему, создавать широковещательные намерения или получать доступ к данным контент-провайдера.