Глава 3. Жизненный цикл Activity Рис. 3.5. Создание фильтра в LogCat Щелкните на кнопке OK ; открывается новая вкладка, в которой выводятся только сообщения с меткой QuizActivity (рис. 3.6). После запуска GeoQuiz были вызваны три метода жизненного цикла и был создан исходный экземпляр QuizActivity Рис. 3.6. Запуск GeoQuiz сопровождается созданием, запуском и продолжением активности (Если вы не видите отфильтрованный список, выберите фильтр QuizActivity на левой панели LogCat.) А теперь немного поэкспериментируем. Нажмите на устройстве кнопку Back , а за- тем проверьте вывод LogCat. Активность приложения получила вызовы onPause() , onStop() и onDestroy() Рис. 3.7. Нажатие кнопки Back приводит к уничтожению активности Нажимая кнопку Back , вы сообщаете Android: «Я завершил работу с активностью, и она мне больше не нужна». Android уничтожает активность, чтобы не избежать неэффективного расходования ограниченных ресурсов устройства.
Использование LogCat 77 Перезапустите приложение GeoQuiz. Нажмите кнопку Home и проверьте вывод LogCat. Ваша активность получила вызовы onPause() и onStop() , но не вызов onDestroy() Рис. 3.8. Нажатие кнопки Home останавливает активность Вызовите на устройстве диспетчер задач. На новых устройствах для этого следует нажать кнопку Recents рядом с кнопкой Home (рис. 3.9). На устройствах без кнопки Recents выполните долгое нажатие кнопки Home Кнопка Recents Кнопка Home Кнопка Back Рис. 3.9. Кнопки Home, Back и Recents В диспетчере задач нажмите на приложении GeoQuiz и проверьте вывод LogCat. Активность запускается и продолжает работу, но создавать ее не нужно. Нажатие кнопки Home сообщает Android: «Я сейчас займусь другим делом, но потом могу вернуться». Android приостанавливает активность, но старается не уничтожать ее на случай возвращения.
78 Глава 3. Жизненный цикл Activity Тем не менее существование остановленной активности не гарантировано. Если системе потребуется занятая память, она уничтожает остановленные активности. Наконец, представьте маленькое временное окно, которое только частично за- крывает активность. При появлении такого окна находящаяся за ним активность приостанавливается и взаимодействие с ней невозможно. Выполнение активности будет продолжено, когда временное окно будет закрыто. Далее в этой книге мы будем переопределять различные методы жизненного цикла активности для решения реальных задач. При этом применение каждого метода будет рассматриваться более подробно. Повороты и жизненный цикл активности А теперь вернемся к ошибке, обнаруженной в конце главы 2. Запустите GeoQuiz, нажмите кнопку Next для перехода к следующему вопросу, а затем поверните устройство. (Чтобы имитировать поворот в эмуляторе, нажмите Control+F12/Ctrl+F12 ). После поворота GeoQuiz снова выводит первый вопрос. Чтобы понять, почему это произошло, просмотрите вывод LogCat. Рис. 3.10. QuizActivity умирает и возрождается Когда вы поворачиваете устройство, экземпляр QuizActivity , который вы видели, уничтожается, и вместо него создается новый экземпляр. Снова поверните устрой- ство — происходит еще один цикл уничтожения и возрождения. Из-за этого и возникает ошибка. При каждом создании нового экземпляра QuizAc- tivity переменная mCurrentIndex инициализируется 0, а пользователь начинает с первого вопроса. Вскоре мы исправим ошибку, но сначала повнимательнее раз- беремся, почему это происходит. Конфигурации устройств и альтернативные ресурсы Поворот приводит к изменению конфигурации устройства. Конфигурация устрой- ства представляет собой набор характеристик, описывающих текущее состояние конкретного устройства. К числу характеристик, определяющих конфигурацию,
Создание макета для альбомной ориентации 79 относится ориентация экрана, плотность пикселов, размер экрана, тип клавиатуры, режим стыковки, язык и многое другое. Как правило, приложения предоставляют альтернативные ресурсы для разных конфигураций устройств. Пример такого рода нам уже встречался — вспомните, как мы включали в проект несколько изображений стрелки для разной плотности пикселов. Плотность пикселов является фиксированным компонентом конфигурации устройства; она не может измениться во время выполнения. Напротив, некоторые компоненты (такие, как ориентация) могут изменяться при выполнении. При изменении конфигурации во время выполнения может оказаться, что при- ложение содержит ресурсы, лучше подходящие для новой конфигурации. Чтобы увидеть, как работает этот механизм, мы создадим альтернативный ресурс, который Android найдет и использует при изменении ориентации экрана. Создание макета для альбомной ориентации Сверните панель LogCat. (Если вместо этого вы случайно закроете панель LogCat, ее всегда можно открыть заново командой Window Show View... ) Затем на панели Package Explorer щелкните правой кнопкой мыши на каталоге res и создайте новую папку. Присвойте ей имя layout-land Рис. 3.11. Создание новой папки
80 Глава 3. Жизненный цикл Activity Скопируйте файл activity_quiz.xml из res/layout/ в res/layout-land/ . Теперь в приложении имеются два макета: макет для альбомной ориентации и макет по умолчанию. Оставьте имя файла без изменения. Два файла макетов должны иметь одинаковые имена, чтобы на них можно было ссылаться по одному идентификатору ресурса. Суффикс -land — еще один пример конфигурационного квалификатора. По кон- фигурационным квалификаторам подкаталогов res Android определяет, какие ресурсы лучше всего подходят для текущей конфигурации устройства. Список конфигурационных квалификаторов, поддерживаемых Android, и обозначаемых ими компонентов конфигурации устройств находится по адресу https://developer. android.com/guide/topics/resources/providing-resources.html . Мы вернемся к исполь- зованию конфигурационных квалификаторов в главе 15. Когда устройство находится в альбомной ориентации, Android находит и исполь- зует ресурсы в каталоге res/layout-land . В противном случае используются ресурсы по умолчанию из каталога res/layout/ Внесем некоторые изменения в альбомный макет, чтобы он отличался от макета по умолчанию. Сводка этих изменений представлена на рис. 3.12. Рис. 3.12. Альтернативный макет для альбомной ориентации Вместо LinearLayout будет использоваться FrameLayout — простейшая разно- видность ViewGroup без определенного способа размещения потомков. В этом макете дочерние представления будут размещаться в соответствии с атрибутом android:layout_gravity Атрибут android:layout_gravity необходим виджетам TextView , LinearLayout и Button . Потомки Button виджета LinearLayout остаются без изменений. Откройте файл layout-land/activity_quiz.xml и внесите необходимые изменения, руко- водствуясь рис. 3.12. Проверьте результат своей работы по листингу 3.4.
Снова запустите GeoQuiz. Поверните устройство в альбомную ориентацию, чтобы увидеть новый макет. Конечно, в программе используется не только новый макет, но и новый экземпляр QuizActivity Поверните устройство, чтобы переключиться в книжную ориентацию. Вы увидите новый макет — и новый экземпляр QuizActivity Android выбирает наиболее подходящий ресурс за вас, но для этого он создает новую активность «с нуля». Чтобы класс QuizActivity вывел новый макет, не- обходимо снова вызвать метод setContentView(R.layout.activity_quiz) . А это не произойдет без повторного вызова QuizActivity.onCreate(…) . Соответственно
82 Глава 3. Жизненный цикл Activity Android при повороте уничтожает текущий экземпляр QuizActivity и начинает все заново, чтобы обеспечить оптимальный подбор ресурсов для новой конфи- гурации. Рис. 3.13. QuizActivity в альбомной ориентации Учтите, что Android уничтожает текущую активность и создает новую при каждом изменении конфигурации времени выполнения. Также во время выполнения могут происходит и другие изменения (например, изменение языка или доступности клавиатуры), но изменение в ориентации экрана является самым частым. Сохранение данных между поворотами Android очень старается предоставить альтернативные ресурсы в нужный момент. Тем не менее уничтожение и повторное создание активностей при поворотах мо- жет создать проблемы — как, например, в случае с возвратом к первому вопросу в приложении GeoQuiz. Чтобы исправить эту ошибку, экземпляр QuizActivity , созданный после поворота, должен знать старое значение mCurrentIndex . Нам необходим механизм сохранения данных при изменении конфигурации времени выполнения (например, при поворо- тах). Одно из возможных решений заключается в переопределении метода Activity protected void onSaveInstanceState(Bundle outState) Обычно этот метод вызывается системой перед onPause() , onStop() и onDestroy() Реализация по умолчанию onSaveInstanceState(…) приказывает всем представле- ниям активности сохранить свое состояние в данных объекта Bundle — структуры, связывающей строковые ключи со значениями некоторых ограниченных типов. Мы уже видели тип Bundle . Он передается методу onCreate(Bundle) : @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
Переопределение onSaveInstanceState(Bundle) 83 При переопределении onCreate(…) вы вызываете реализацию onCreate(…) суперклас- са активности и передаете ей только что полученный объект Bundle . В реализации суперкласса сохраненное состояние представлений извлекается и используется для воссоздания иерархии представлений активности. Переопределение onSaveInstanceState(Bundle) Метод onSaveInstanceState(…) можно переопределить так, чтобы он сохранял до- полнительные данные в Bundle , а затем снова загружал их в onCreate(…) . Именно так мы организуем сохранение значения mCurrentIndex между поворотами. Для начала добавьте в QuizActivity.java константу, которая станет ключом в сохраня- емой паре «ключ-значение». Листинг 3.5. Добавление ключа для сохраняемого значения (QuizActivity.java) public class QuizActivity extends Activity { private static final String TAG = "QuizActivity"; private static final String KEY_INDEX = "index"; Button mTrueButton; Теперь переопределим onSaveInstanceState(…) для записи значения mCurrentIndex в Bundle с использованием константы в качестве ключа. Листинг 3.6. Переопределение onSaveInstanceState(…) (QuizActivity.java) mNextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mCurrentIndex = (mCurrentIndex + 1) % mAnswerKey.length; updateQuestion(); } }); updateQuestion(); } @Override public void onSaveInstanceState(Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); Log.i(TAG, "onSaveInstanceState"); savedInstanceState.putInt(KEY_INDEX, mCurrentIndex); } Наконец, в методе onCreate(…) следует проверить это значение, и если оно при- сутствует — присвоить его mCurrentIndex
84 Глава 3. Жизненный цикл Activity Листинг 3.7. Проверка сохраненных данных в onCreate(…) (QuizActivity.java) if (savedInstanceState != null) { mCurrentIndex = savedInstanceState.getInt(KEY_INDEX, 0); } updateQuestion(); } Запустите GeoQuiz и нажмите кнопку Next . Сколько бы поворотов устройства вы ни выполнили, вновь созданный экземпляр QuizActivity «вспоминает» текущий вопрос. Учтите, что для сохранения и восстановления из Bundle подходят примитивные типы и объекты, реализующие интерфейс Serializable . Создавая собственные классы, которые вы планируете сохранять в onSaveInstanceState(…) , не забудьте реализовать Serializable Реализацию onSaveInstanceState(…) желательно протестировать — особенно если вы сохраняете и восстанавливаете объекты. Повороты тестируются легко; с тести- рованием ситуаций нехватки памяти дело обстоит сложнее. В конце этой главы приведена информация о том, как имитировать уничтожение вашей активности системой Android для освобождения памяти. Снова о жизненном цикле Activity Переопределение onSaveInstanceState(Bundle) используется не только при пово- ротах. Активность также может уничтожаться и в том случае, если пользователь отойдет от устройства, а Android понадобится освободить память. Android никогда не уничтожает для освобождения памяти выполняемую активность. Чтобы активность была уничтожена, она должна находиться в приостановленном или остановленном состоянии. Если активность приостановлена или остановлена, значит, был вызван ее метод onSaveInstanceState(…) При вызове onSaveInstanceState(…) данные сохраняются в объекте Bundle . Опе- рационная система заносит объект Bundle в запись активности (activity record). Чтобы понять, что собой представляет запись активности, добавим сохраненное состояние на схему жизненного цикла активности (рис. 3.14). Когда ваша активность сохранена, объект Activity не существует, но объект записи активности «живет» в ОС. При необходимости операционная система может вос- создать активность по записи активности. Следует учесть, что активность может перейти в сохраненное состояние без вызова onDestroy() . Однако вы всегда можете рассчитывать на то, что методы onPause() и onSaveInstanceState(…) были вызваны. Обычно метод onSaveInstanceState(…) переопределяется для сохранения данных в Bundle , а onPause() — для всех прочих операций.
Для любознательных: тестирование onSaveInstanceState(Bundle) 85 Сохранение (эк- земпляр актив- ности уничтожен; состояние актив- ности сохранено) Пользователь возвра- щается к активности, процесс снова начи- нает работать Приложение не существует Остановка (прило- жение невидимо) Выполнение (приложе- ние видимо и находится на переднем плане) Приостановка (прило- жение видимо Запуск Завершается или уничтожается Android Становится видимым для пользователя Перестает быть видимым Уходит с пе- реднего плана Выходит на пе- редний план Рис. 3.14. Полный жизненный цикл активности В некоторых ситуациях Android не только уничтожает активность, но и полностью завершает процесс приложения. Такая ситуация может возникнуть только в том случае, если пользователь не смотрит на приложение, но она возможна. Даже в этом случае запись активности продолжает существовать и позволяет быстро переза- пустить активность при возвращении пользователя. Когда же запись приложения пропадает? Когда пользователь нажимает кнопку Back , активность уничтожается — раз и навсегда. При этом запись активности теряется. Записи активности также обычно уничтожаются при перезагрузке; также возможно их уничтожение в том случае, если они слишком долго не используются. Для любознательных: тестирование onSaveInstanceState(Bundle) Переопределяя onSaveInstanceState(Bundle) , необходимо убедиться в том, что состояние сохраняется и восстанавливается так, как предполагалось. Это легко делается в эмуляторе.
86 Глава 3. Жизненный цикл Activity Запустите виртуальное устройство. В списке приложений на устройстве найдите приложение Settings . Оно присутствует в большинстве образов системы, использу- емых в эмуляторе. Рис. 3.15. Поиск приложения Settings Запустите приложение Settings и выберите категорию Development options . В нее входит много разных настроек; установите флажок Don’t keep activities Запустите приложение и нажмите кнопку Home . Это приведет к приостановке и остановке активности. Затем остановленная активность будет уничтожена так, как если бы ОС Android решила освободить занимаемую ей память. Восстановите приложение и посмотрите, было ли состояние сохранено так, как ожидалось. Нажатие кнопки Back (вместо Home ) всегда приводит к уничтожению активности независимо от того, установлен флажок в настройках разработчика или нет. На- жатие кнопки Back сообщает ОС, что пользователь завершил работу с активностью. Чтобы выполнить тот же тест на физическом устройстве, необходимо установить на нем пакет Dev Tools. За дополнительной информацией обращайтесь по адресу https://developer.android.com/tools/debugging/debugging-devtools.html
Для любознательных: методы и уровни регистрации 87 Рис. 3.16. Запрет сохранения активностей Для любознательных: методы и уровни регистрации Когда вы используете класс android.util.Log для регистрации сообщений в жур- нале, вы задаете не только содержимое сообщения, но и уровень регистрации, опре- деляющий важность сообщения. Android поддерживает пять уровней регистрации (табл. 3.1). Каждому уровню соответствует свой метод класса Log . Регистрация данных в журнале сводится к вызову соответствующего метода Log Таблица 3.1. Методы и уровни регистрации Уровень регистрации Метод Примечания ERROR Log.e(…) Ошибки WARNING Log.w(…) Предупреждения INFO Log.i(…) Информационные сообщения DEBUG Log.d(…) Отладочный вывод (может фильтроваться) VERBOSE Log.v(…) Только для разработчиков!