Главная » Разработка для Android » AsyncTask и поток пользовательского интерфейса – Android

0

 

Если вам приходилось работать с современными фреймворками пользовательских интерфейсов, то фреймворк пользовательского интерфейса Android покажется вам очень знакомым. Этот интерфейс событийно-управляемый, основан на библиотеке вкладываемых друг в друга (nestable) компонентов. И что особенно важно в данном случае, этот фреймворк однопоточный. Уже много лет назад разработчики обнаружили, что, поскольку графический пользовательский интерфейс должен реагировать на асинхронные события, поступающие из нескольких источников, в многопоточном пользовательском интерфейсе практически невозможно избежать взаимоблокировки. Напротив, один и тот же поток должен обслуживать как ввод (сенсорный экран, клавиатура и т. д.), так и вывод (например, дисплей). Он выполняет запросы, поступающие из этих источников, и делает это последовательно, в том порядке, как получает запросы.

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

В Android есть удобный инструмент для реализации асинхронных задач, он называется AsyncTask. Он полностью скрывает многие детали потоков, используемых для выполнения задачи.

Рассмотрим предельно упрощенное приложение, инициализирующее игровой движок. Это приложение будет отображать какую-нибудь вводную графику, пока загружается контент. На рис. 6.1 показан простейший пример такого приложения. Когда вы нажимаете кнопку, оно инициализирует игровой уровень, а потом отображает в текстовом поле приветствие.

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

Рис. 6.1. Простое приложение для инициализации игры

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

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

Этот код уже очень близок к тому варианту, который нужен, чтобы приложение-пример работало правильно. Но он содержит один очень пагубный недостаток: код блокирует поток пользовательского интерфейса на все то время, пока длится вызов к game .initialize. Такая ситуация чревата всяческими неприятными эффектами.

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

И это еще полбеды. К тому же фреймворк Android отслеживает потоки пользовательского интерфейса приложения, чтобы помешать неисправным или вредоносным программам вызвать зависание устройства. Если приложение слишком долго не отвечает на ввод, фреймворк приостанавливает его исполнение, уведомляет пользователя о возникшей проблеме и предлагает возможность принудительно закрыть это приложение. Если построить и запустить это приложение, как показано выше, в частности реализовать initGame именно так, как в примере (попробуйте, пример поучительный), то, как только вы нажмете кнопку Send Request (Отправить запрос), интерфейс зависнет. Если нажмете еще пару раз, то увидите предупреждение, примерно как на рис. 6.2.

Рис. 6.2. Приложение, которое не отвечает

Вот здесь нам и пригодится AsyncTask! Этот класс в Android является относительно безопасным, мощным и простым способом выполнения фоновых задач. Вот новая реализация initGame в виде AsyncTask:

Данный код практически идентичен коду из первого примера. Он разделен на три метода, исполняющих примерно тот же код, что и выше, в том же порядке, что HBinitGame.

Этот AsyncTask создан в потоке пользовательского интерфейса. Когда поток пользовательского интерфейса активирует метод execute, относящийся к задаче, то сначала к потоку пользовательского интерфейса применяется метод onPreExecute. Таким образом, задача может инициализировать себя и свое окружение – в данном случае установить фоновую анимацию. Потом AsyncTask создает новый фоновый поток, для параллельного выполнения метода doTnBackground. Когда наконец выполнение doInBackground завершается, фоновый поток удаляется, а метод onPostExecute активируется, опять же в потоке пользовательского интерфейса.

Если допустить, что данная реализация AsyncTask является правильной, то слушателю щелчков (click listener) требуется просто создать экземпляр и активировать его, вот так:

Действительно, код AsyncInitGame теперь является полным, правильным и надежным. Рассмотрим его подробнее.

Для начала отметим, что базовый класс AsyncTask является абстрактным. Единственный способ воспользоваться им – это создать подкласс, специально предназначенный для выполнения конкретной задачи (в виде отношения is-a, а не has-a). Как правило, подкласс будет простым, анонимным и будет определять всего несколько методов. С учетом хорошего стиля и разделения функций проблем, нужно, чтобы подкласс был небольшим и делегировал реализацию тем классам, которые отвечают соответственно за пользовательский интерфейс и за асинхронную задачу. В данном примере, в частности, doInBackground – это просто посредник класса Game.

Вообще, AsyncTask принимает набор параметров и возвращает результат. Поскольку параметры должны передаваться между потоками, а результат – возвращаться между потоками, необходимо квитирование установления связи (так называемое рукопожатие), обеспечивающее безопасность потоков. AsyncTask активируется при вызове его метода execute с некоторыми параметрами. Эти параметры в конечном счете передаются методу doInBackground, работающему в фоновом потоке. Делается это посредством механизма AsyncTask. В свою очередь, doInBackground выдает результат. Механизм AsyncTask возвращает этот результат, передавая его в качестве аргумента doPostExecute, работающему в том же потоке, что и исходный execute. На рис. 6.3 показан поток данных.

Рис. 6.3. Поток данных в AsyncTask

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

При определении конкретного подкласса AsyncTask вы указываете точные типы Params, Progress и Result – это переменные типов в определении AsyncTask. Первый и последний из типов этих переменных (Params и Result) – это соответственно типы параметров и результата задачи. Средний тип переменных мы также вскоре обсудим.

Конкретный тип, связываемый с Params, – это тип параметров, передаваемых в execute, и, следовательно, тип параметров, которые передаются в doInBackground. Аналогично конкретный тип, связанный с Resul t, – это тип значения, возвращаемого из doInBackground, а значит, тип параметра, который передается в onPostExecute.

Синтаксический разбор всего этого несколько сложен, и первый пример, AsyncInitGame, не особенно полезен, так как в нем и параметр, и результат относятся к одному и тому же типу – String. Ниже приведена пара примеров, в которых типы параметров и результатов отличаются. Они лучше иллюстрируют использование переменных универсальных типов:

В первом примере аргументом метода execute экземпляра AsyncDBReq будет одна или несколько переменных PreparedStatement. Реализация метода doInBackground для экземпляра AsyncDBReq примет параметры этой переменной PreparedStatement в качестве своих аргументов и возвратит ResultSet. Экземпляр метода onPostExecute примет этот ResultSet в качестве параметра и использует его по назначению.

Аналогично во втором примере вызываемый метод execute экземпляра AsyncHttpReq примет одну или несколько переменных HttpRequest. Метод doInBackground примет эти запросы в качестве параметров и возвратит HttpResponse. В свою очередь, onPostExecute обработает HttpResponse.

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

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

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

Хотя здесь совершенно не из-за чего выводить предупреждения о проблеме – нет ошибок компиляции, предупреждений времени исполнения (runtime warning) и, кроме того, при возникновении ошибки не происходит немедленного краха системы, – этот код совершенно неправильный. К переменной mCount получают доступ два различных потока, между которыми не происходит синхронизации.

С учетом сказанного вы можете удивиться тому, что доступ к mlnFTight не синхронизирован с доступом к AsyncTask Demo. Вот здесь как раз все нормально. Контракт AsyncTask гарантирует, что методы onPreExecute и onPostExecute будут запускаться в том же потоке, из которого был вызван execute. В отличие от mCount, доступ к mlnFlight осуществляется из единственного потока – следовательно, синхронизация при этом не требуется.

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

Проблема довольно тонкая. Вы, возможно, заметили, что на vals (аргумент для initButton) ставятся параллельные (конкурентные) ссылки. И это происходит без синхронизации – вот в чем дело! Vals передается к AsyncTask как аргумент execute. Это происходит при активации задачи. Фреймворк AsyncTask может гарантировать, что эта ссылка будет правильно опубликована в фоновом потоке, когда вызывается doInBackground. При этом ничего не удастся сделать со ссылкой на vals, которая удерживается, а позднее применяется в методе initButton. Вызов к vals.clear изменяет состояние, которое используется в другом потоке без синхронизации. Следовательно, безопасность потоков нарушается.

Чтобы решить такую проблему, лучше всего убедиться, что аргументы для AsyncTask являются постоянными. Если их нельзя изменить (как String или Integer) или они являются POJO (Plain Old Java Objects, старые добрые объекты Java), содержащими только финальные поля, то безопасность потоков соблюдается и дальнейшей обработки не требуется. Если к AsyncTask передается объект, который может быть изменен, то единственный способ гарантировать безопасность потоков – убедиться, что ссылку на него удерживает только AsyncTask. Поскольку в предыдущем примере (см. рис. 6.1) параметр vals передается методу (initButton), мы совершенно не можем гарантировать, что на этот параметр не будет «висячих» ссылок. Даже после удаления вызова vals.clear нельзя быть уверенными, что код станет правильным, так как сторона, вызывающая initButton, может сохранить ссылку на карту, которая в конечном итоге будет передана как параметр vals. Единственный способ добиться правильности этого кода – сделать полную (глубокую) копию карты и всех содержащихся в ней объектов!

Разработчики, знакомые с пакетом Java Collections, могут возразить, что можно обойтись и без полной глубокой копии параметров карты, а применить в качестве обертки unmodifіablеМар:

К сожалению, код остался неправильным. Collections.unmodifiableMap обеспечивает постоянство вида той карты, которая в ней обернута. Однако такая обертка не закрывает доступ к объекту таким процессам, которые удерживают ссылку на оригинальный, изменяемый вариант такого объекта – и процессы, обладающие такими ссылками, действительно могут изменить его когда угодно. В предыдущем примере, хотя AsyncTask и не может изменить значение карты, переданной ему в методе execute, метод onCl ickListener все равно изменяет карту со ссылкой на vals. В то же время фоновый поток использует эту карту без синхронизации. Опа!

Завершая этот подраздел, отметим, что в примере есть еще один метод, используемый AsyncTask: onProgressUpdate. Этот метод нужен для того, чтобы долгоиграющие задачи могли периодически отправлять в пользовательский интерфейс статусные сообщения и эти операции были безопасными. Здесь, возможно, будет целесообразно реализовать индикатор загрузки, показывающий, когда завершится инициализация игры:

В данном примере предполагается, что при инициализации игры в качестве аргумента принимается Game. InitProgressListener. В процессе инициализации периодически вызывается метод onlnitProgress, который должен уведомить, какая часть работы выполнена. Затем в этом примере onlnitProgress будет вызван «из-под» doInBackground, если смотреть по дереву вызовов, то есть вызов будет происходить в фоновом потоке. Если бы onlnitProgress вызывал AsyncTaskDemoWithProgress. updateProgressBar напрямую, то последующий вызов bar. setStatus также происходил бы в фоновом потоке, нарушая правило, в соответствии с которым только поток пользовательского интерфейса может изменять объекты View. Получилось бы подобное исключение:

Чтобы правильно опубликовать информацию о ходе загрузки и передать ее обратно в поток пользовательского интерфейса, onlnitProgress вызывает метод publ і shProgress, относящийся к AsyncTask. В свою очередь, AsyncTask обрабатывает детали диспетчирования publ і shProgress в потоке пользовательского интерфейса, так что onProgressUpdate может свободно использовать методы View.

Завершим детальное рассмотрение AsyncTask суммированием основных его аспектов.

Пользовательский интерфейс Android является однопоточным. Чтобы правильно обращаться с ним, разработчик должен хорошо ориентироваться в работе с идиомой постановки задач в очередь.

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

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

AsyncTask – удобный инструмент для выполнения небольших асинхронных задач. Просто не забывайте, что метод doInBackground работает в другом потоке! Он не должен записывать никакое состояние, видимое из другого потока или считывать состояние, доступное для изменений из другого потока. Это же касается его параметров.

Постоянные (immutable) объекты – важный инструмент для передачи информации между параллельными потоками.

Источник: Android. Программирование на Java для нового поколения мобильных устройств

По теме:

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