Главная » Программирование игр под Android » Непрерывная прорисовка с SurfaceView – РАЗРАБОТКА ИГР ДЛЯ ОС ANDROID

0

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

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

Как намекает его имя, класс SurfaceView наследуется от View и обрабатывает Surface, еще один класс Android API. Что такое Surface? Это абстракция необработанного буфера, используемого компоновщиком экрана для прорисовки определенного View. Компоновщик экрана – это властелин всего рендеринга в Android, ответственный за передачу всех пикселов графическому процессору. В некоторых случаях Surface может быть аппаратно ускорен, но мы не будем сильно задумываться над этим фактом. Все, что нам необходимо знать, что есть более прямой способ прорисовки элементов на экране.

Наша цель – обеспечить прорисовку в отдельном потоке, чтобы не трогать поток пользовательского интерфейса, занятый другими делами. Класс SurfaceVi ew предлагает нам способ прорисовки в отдельном потоке.

SurfaceHolder и блокировка. Чтобы рисовать SurfaceView из отдельного от пользовательского потока, нам необходимо получить экземпляр класса Surf aceHolder, примерно так:

SurfaceHolder – оболочка для Surface, проводящая для нас некоторые действия с помощью двух методов:

Первый метод блокирует Surface для прорисовки и возвращает прекрасный экземпляр Canvas, который мы можем использовать. Второй метод вновь разблокирует Surface и проверяет, что прорисованный нами через Canvas контент выведен на экран. Мы будем применять эти два метода в нашем потоке прорисовки для получения Canvas, собственно рисования и вывода полученного изображения на экран. Объект Canvas, передаваемый в метод Surf aceHol der. unlockAndPost О, должен быть тем же самым, который мы получили от SurfaceHolder. lockCanvas.

Surface не создается немедленно после инициализации SurfaceView – это делается асинхронно. Surface будет уничтожаться каждый раз при приостановке активности и создаваться вновь при ее возобновлении.

Создание и проверка Surface. Мы не можем получить Canvas от SurfaceHolder до тех пор, пока Surface не является корректным. У нас есть возможность проверить факт его создания с помощью следующего выражения:

Если метод возвращает true, мы можем без опаски блокировать Surface и рисовать в нем с помощью полученного Canvas. Нам необходимо быть абсолютно уверенными, что Surface вновь разблокирован после вызова Surf aceHolder. 1 ockCanvas , иначе наша активность может заблокировать телефон.

Все вместе

Итак, как нам теперь объединить все это с отдельным потоком прорисовки и жизненным циклом активности? Лучший способ представить это – посмотреть на реальный пример кода. В листинге 4.16 представлен полный пример, выполняющий прорисовку в отдельном потоке при помощи SurfaceView.

Листинг 4.16. Активность SurfaceViewTest package com.badlogi с.androi dgames;

Выглядит пугающе, не правда ли? Наша активность содержит экземпляр FastRenderView в качестве члена. Это наш подкласс SurfaceView, предназначенный для обработки всех потоковых операций и блокировки Surface. С точки зрения активности – это простой View. В методе onCreate мы включаем полноэкранный режим, создаем экземпляр FastRenderView и устанавливаем его в качестве контейнера содержимого активности.

Кроме того, в этот раз мы переопределяем метод onResume О. В нем стартуем наш поток прорисовки косвенным образом – вызываем метод FastRenderView. resume, делающий эту работу незаметно для нас. Это значит, что поток запустится, когда активность создается впервые (поскольку вслед за onCreate всегда вызывается onResume). Поток также перезапускается при каждом возвращении активности из состояния паузы. Безусловно, это подразумевает, что где-то поток должен останавливаться, иначе у нас при каждом вызове onResume будет создаваться еще один новый поток. Для этого мы используем метод onPauseO он вызывает метод FastRenderView. pauset), полностью останавливающий поток. Метод не будет заканчивать свою работу, пока поток действительно не остановится.

Взглянем на ключевой класс этого примера: FastRenderView. Он похож на классы RenderView, реализованные нами в нескольких прошлых примерах, тем, что наследуется от другого класса View. В данном случае мы наследуем его напрямую от SurfaceView и реализуем в нем интерфейс Runnable, чтобы иметь возможность передавать его потоку прорисовки и использовать его логику.

Класс FastRenderView имеет три члена. renderThread – просто ссылка на экземпляр Thread, ответственный за выполнение логики потока прорисовки. Переменная holder – ссылка на экземпляр SurfaceHolder, полученный нами от базового класса Surf aceView. Наконец, член runni ng – простой логический флаг, используемый нами для сообщения потоку прорисовки о том, что он должен остановиться. Модификатор volatile имеет специальное назначение, о котором мы поговорим чуть позже. В конструкторе мы лишь вызываем конструктор базового класса и сохраняем ссылку на SurfaceHolder в переменной класса.

Теперь приходит время метода FastRenderView. resumeC). Он ответственен за запуск потока прорисовки. Обратите внимание – мы создаем новый экземпляр потока каждый раз при вызове этого метода. Это соответствует тому, что мы говорили о методах активности onResumeO и onPauseC). Помимо этого мы устанавливаем флаг running в значение true (чуть позже вы увидите, как он используется в потоке прорисовки). Последнее, что следует уточнить, – мы установили экземпляр FastRenderVi ew реализующим интерфейс Runnable, что позволит выполнить следующий метод FastRenderVi ew в этом новом потоке.

Метод FastRenderVi ew. run С выполняет основную работу для нашего класса View. Его тело выполняется в потоке прорисовки. Как видите, он состоит из одного цикла, прекращающего свое выполнение при установке флага running в f al se. Если это происходит, поток останавливается и уничтожается. Внутри цикла while мы сначала проверяем валидность Surface и при положительном результате блокируем его, рисуем в нем и вновь разблокируем (как говорилось ранее). В данном примере мы просто заполняем весь Surface красным цветом.

Метод FastRenderView. pauseC выглядит немного странно. В его начале мы устанавливаем флаг running равным false (это сигнал для метода FastRenderView.runO остановить обработку и закрыть поток). Следующие несколько строк – ожидание полного уничтожения потока, осуществляемое вызовом метода Thread. joinO. Этот метод ждет уничтожения потока, но может также вызвать исключение InterruptedExcepti on до того, как поток на самом деле сгинет. Поскольку нам необходимо быть абсолютно уверенными в уничтожении потока, прежде чем возвращаться из этого метода, мы выполняем Thread. joinO в безусловном цикле до тех пор, пока операция не будет завершена успешно.

Вернемся к модификатору volatiе флага running. Для чего он нужен? Причина довольно тонкая: компилятор может решить переопределить порядок выражений в методе FastRenderView.pauseC, если распознает отсутствие зависимости между первой строкой метода и блоком while. У него есть такое право, если он считает, что это приведет к ускорению выполнения кода. Однако в данном случае порядок команд для нас весьма важен. Представьте, что мы установили флаг running после попытки присоединиться к потоку. Мы получим бесконечный цикл, поскольку поток никогда не будет уничтожен.

Модификатор volatile предотвращает эту коллизию. Любые выражения, связанные с переменными, которые обладают этим модификатором, будут выполнены в установленном порядке. Это уберегает нас от скрытой ошибки, которую невозможно будет воспроизвести.

Осталась еще одна вещь, о которой вы могли задуматься как о потенциальном баге. Что произойдет, если Surface будет уничтожен между вызовами SurfaceHolder. getSurface.isVal id и SurfaceHolder. lock? К счастью, нам повезло – такого никогда не произойдет. Что понять почему, нам нужно вернуться к теме жизненного цикла активности и его связи с Surface.

Мы знаем, что Surface создается асинхронно. Похоже, что наш поток прорисовки может выполниться до того, как Surface станет валидным. Защита от этого казуса состоит в том, что мы не блокируем Surface до тех пор, пока он не станет валидным, что решает проблему его создания.

Причина, по которой код потока прорисовки не вызывает ошибки при уничтожении Surface между проверкой валидности и блокировкой, связана с моментом времени, когда Surface уничтожается. Это всегда происходит после возвращения из метода onPause активности. И поскольку в этом методе мы ожидаем уничтожения потока с помощью FastRenderView.pause, поток прорисовки уже точно не будет живым при уничтожении Surface. Здорово, не так ли? Однако тут легко можно сбиться с толку.

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

Источник: Mario Zechner / Марио Цехнер, «Программирование игр под Android», пер. Егор Сидорович, Евгений Зазноба, Издательство «Питер»

По теме:

  • Комментарии