AsyncTask і потік користувальницького інтерфейсу – Android

&nbsp

Якщо вам доводилося працювати з сучасними фреймворками користувальницьких інтерфейсів, то фреймворк для користувача інтерфейсу Android здасться вам дуже знайомим Цей інтерфейс подієво-керований, заснований на бібліотеці вкладених один в одного (nestable) компонентів І що особливо важливо в даному випадку, цей фреймворк однопотоковий Вже багато років тому розробники виявили, що, оскільки графічний користувальницький інтерфейс повинен реагувати на асинхронні події, що надходять з декількох джерел, в багатопотоковому інтерфейсі практично неможливо уникнути взаимоблокировки Навпаки, один і той же потік повинен обслуговувати як введення (сенсорний екран, клавіатура і т д), так і висновок (наприклад, дисплей) Він виконує запити, що надходять з цих джерел, і робить це послідовно, в тому порядку, як отримує запити

У той час як користувальницький інтерфейс працює на одному потоці, практично будь-яке нетривіальне додаток Android буде багатопотоковим Наприклад, користувальницький інтерфейс повинен відповідати на дії користувача і анімі-ровать дисплей, незалежно від того, чи зайнятий в даний момент обробкою вхідних даних той код, який отримує дані з мережі Інтерфейс користувача повинен працювати швидко, зокрема швидко реагувати, і принципово не може функціонувати «з оглядкою» на інші, довготривалі процеси Довготривалі процеси повинні працювати асинхронно

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

Розглянемо гранично спрощене додаток, инициализирующее ігровий движок Ця програма буде відображати якусь вступну графіком, поки завантажується контент На рис 61 показаний найпростіший приклад такого додатка Коли ви натискаєте кнопку, воно ініціалізує ігровий рівень, а потім відображає в текстовому полі привітання

Це шаблонний (так званий boilerplate) код програми У ньому не вистачає лише тієї частини, яка, власне, ініціалізує гру і оновлює текстове поле:

Рис 61 Просте додаток для ініціалізації гри

А тепер припустимо, що в цьому прикладі ми хочемо відобразити просто анімований фон (повзуть точки з рис 61), поки користувач чекає ініціалізації гри Ось начерк коду, який для цього знадобиться-

Тут все дуже просто Користувач може кілька разів натиснути кнопку запуску гри, тому одночасно може початися кілька ініціалізацій Якщо фон-заставка ще не відображається, покажіть його, памятаючи при цьому, що зараз запускається ще одна гра Потім зробіть повільний виклик до Ініціалізатор движка гри Коли ініціалізація гри завершиться, виконайте очистку Якщо закінчилася ініціалізація останньої запущеної гри, приберіть вступну заставку Нарешті, відобразіть в текстовому полі привітання

Цей код вже дуже близький до того варіанту, який потрібен, щоб додаток-приклад працювало правильно Але він містить один дуже згубний недолік: код блокує потік користувальницького інтерфейсу на всі той час, поки триває виклик до game initialize Така ситуація чревата усілякими неприємними ефектами

Найбільш явний подібний ефект полягає в тому, що не працюватиме фонова анімація Навіть хоча логіка настройки і запуску анімації майже правильна, з коду цілком ясно випливає, що в призначеному для користувача інтерфейсі не повинно відбуватися рівно нічого – до тих пір, поки не завершиться виклик віддаленої процедури

І це ще півбіди До того ж фреймворк Android відстежує потоки користувальницького інтерфейсу додатку, щоб перешкодити несправним або шкідливим програмам викликати зависання пристрою Якщо додаток занадто довго не відповідає на введення, фреймворк зупиняє його виконання, повідомляє користувача про проблему і пропонує можливість примусово закрити цей додаток Якщо побудувати і запустити цю програму, як показано вище, зокрема реалізувати initGame саме так, як у прикладі (спробуйте, приклад повчальний), то, як тільки ви натиснете кнопку Send Request (Відправити запит), інтерфейс зависне Якщо натиснете ще пару раз, то побачите попередження, приблизно як на рис 62

Рис 62 Додаток, який не відповідає

Ось тут нам і стане в нагоді 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 На рис 63 показаний потік даних

Рис 63 Потік даних в 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 Виклик до valsclear змінює стан, який використовується в іншому потоці без синхронізації Отже, безпека потоків порушується

Щоб вирішити таку проблему, найкраще переконатися, що аргументи для AsyncTask є постійними Якщо їх не можна змінити (як String або Integer) або вони є POJO (Plain Old Java Objects, старі добрі обєкти Java), що містять тільки фінальні поля, то безпеку потоків дотримується і подальшої обробки не потрібно Якщо до AsyncTask передається обєкт, який може бути змінений, то єдиний спосіб гарантувати безпеку потоків – переконатися, що посилання на нього утримує тільки AsyncTask Оскільки в попередньому прикладі (див рис 61) параметр vals передається методу (initButton), ми абсолютно не можемо гарантувати, що на цей параметр не буде «висячих» посилань Навіть після видалення виклику valsclear не можна бути впевненими, що код стане правильним, так як сторона, що викликає initButton, може зберегти посилання на карту, яка в кінцевому підсумку буде передана як параметр vals Єдиний спосіб домогтися правильності цього коду – зробити повну (глибоку) копію карти і всіх розміщених в ній обєктів

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

На жаль, код залишився неправильним CollectionsunmodifiableMap забезпечує сталість виду тієї карти, яка в ній обгорнута Однак така обгортка не закриває доступ до обєкта таким процесам, які утримують посилання на оригінальний, змінюваний варіант такого обєкта – і процеси, що володіють такими посиланнями, дійсно можуть змінити його коли завгодно У попередньому прикладі, хоча 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 для нового покоління мобільних пристроїв

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*