- Потоки
- Использование фоновых потоков
- Плохое приложение
- Запуск потока
- Усыпить поток
- Приоритет потоков
- Отмена выполнения потока
- Многопоточность в Android. Все что вам нужно знать. Часть 1 — Введение
- Многозадачность в Android
- Компоненты многопоточности, которые присоединяются к активности / фрагменту
- AsyncTask
- Загрузчики
- Компоненты многопоточности, которые не присоединяются к активности / фрагменту
- Service
- IntentService
- Многопоточность в Android. Looper, Handler, HandlerThread. Часть 1.
Потоки
Потоки позволяют выполнять несколько задач одновременно, не мешая друг другу, что даёт возможность эффективно использовать системные ресурсы. Потоки используются в тех случаях, когда одно долгоиграющее действие не должно мешать другим действиям. Например, у нас есть музыкальный проигрыватель с кнопками воспроизведения и паузы. Если вы нажимаете кнопку воспроизведения и у вас запускается музыкальный файл в отдельном потоке, то вы не можете нажать на кнопку паузы, пока файл не воспроизведётся полностью. С помощью потоков вы можете обойти данное ограничение.
Использование фоновых потоков
Чтобы быть уверенным, что ваше приложение не теряет отзывчивости, хорошим решением станет перемещение всех медленных, трудоёмких операций из главного потока приложения в дочерний.
Применение фоновых потоков — необходимое условие, если вы хотите избежать появления диалогового окна для принудительного закрытия приложения. Когда активность в Android на протяжении 5 секунд не отвечает на события пользовательского ввода (например, нажатие кнопки) или приёмник широковещательных намерений не завершает работу обработчика onReceive() в течение 10 секунд, считается, что приложение зависло. Подобные ситуации следует избегать любой ценой. Используйте фоновые потоки для всех трудоёмких операций, включая работу с файлами, сетевые запросы, транзакции в базах данных и сложные вычисления.
Android предоставляет несколько механизмов перемещения функциональности в фоновый режим.
- Activity.runOnUiThread(Runnable)
- View.post(Runnable)
- View.postDelayed(Runnable, long)
- Handlers
- AsyncTask
Класс AsyncTask позволяет определить операции, которые будут выполняться в фоне, вы также будете иметь доступ к обработчику событий, что позволит отслеживать прогресс выполнения задач и выводить результаты в контексте главного графического потока. Подробнее об этом классе в отдельной статье.
Хотя использование AsyncTask — хорошее решение, случается, что для работы в фоновом режиме приходится создавать собственные потоки и управлять ими.
В Java есть стандартный класс Thread, который вы можете использовать следующим образом:
Данный способ подходит только для операций, связанных с временем. Но вы не сможете обновлять графический интерфейс программы.
Если вам нужно обновлять интерфейс программы, то нужно использовать AsyncTask, о котором говорилось выше, или вы можете реализовать ваш собственный класс, наследованный от Thread, используя объект Handler из пакета android.os для синхронизации с потоком GUI перед обновлением пользовательского интерфейса.
Вы можете создавать дочерние потоки и управлять ими с помощью класса Handler, а также классов, доступных в пространстве имён java.lang.Thread. Ниже показан простой каркас для переноса операций в дочерний поток.
Плохое приложение
Напишем «плохое» приложение, неправильно использующее основной поток. Однажды мы писали программу для подсчёта ворон. На этот раз будем считать чёрных котов, которые перебегают нам дорогу. Зачем они это делают — молчит наука. Может быть собранная статистика поможет разгадать тайну. Добавим на экран активности кнопки и текстовую метку. Код для щелчка кнопки.
Для имитации тяжёлой работы программа делает паузу на двадцать секунд, а потом выводит текст с подсчётом котов. Если нажать на кнопку один раз и подождать двадцать секунд, то программа отработает как положено. Но представьте себе, что вы нажали на кнопку один раз. Программа запустила паузу. Вы, не дожидаясь окончания паузы, снова нажали на кнопку. Программа должна выполнить вашу команду, но предыдущая команда ещё не отработала и наступает конфликт. Попробуйте нажать на кнопку несколько раз с небольшими перерывами. В какой-то момент приложение зависнет и выведет системное диалоговое окно:
В реальных приложениях такое окно может разозлить пользователя и он поставит низкую оценку вашему приложению.
В данном случае ошибку вызывает не сам вывод текста в текстовой метке, который, к слову, тоже выполняется в основном потоке, а сам щелчок кнопки. Если вы закомментируете последние две строчки кода, связанные с TextView, то ошибка сохранится.
Вам необходимо перенести трудоёмкую задачу в отдельный поток. Для этого создаётся экземпляр класса Runnable, у которого есть метод run(). Далее создаётся объект Thread, в конструкторе у которого указывается созданный Runnable. После этого можно запускать новый поток с помощью метода start(). Перепишем пример.
Весь код мы перенесли в метод run(). Теперь вы можете безостановочно щёлкать по кнопке. На этот раз приложение сохранит свою работоспособность. Чтобы в этом убедиться, в код добавлено протоколирование логов Log.i(). При каждом нажатии создаётся новый поток, в котором выполняется код. Потоки друг другу не мешают и дожидаются своей очереди, когда система позволит им отработать.
Основной поток также называют UI-потоком. Имено в главном потоке можно обновить текст у текстовой метки. В создаваемых нами потоках это делать нельзя. Если вы уберёте комментарии с последнего примера и запустите проект, то получите сообщение об ошибке.
Нужен некий посредник между создаваемыми потоками и основным UI-потоком. В роли такого посредника служит класс Handler (полное название класса android.os.Handler, не перепутайте). Вам нужно создать экземпляр класса и указать код, который нужно выполнить.
После строчки кода с Log.i() добавьте вызов метода посредника.
Поток вызывает посредника, который в свою очередь обновляет интерфейс. В нашем случае посредник посылает пустое сообщение от потока.
Но бывает так, что от потока требуется получить информацию для обработки. Ниже упрощённый пример.
Запуск потока
Предположим, мы разрабатываем собственный проигрыватель. У нас есть кнопка Play, которая вызывает метод play() для воспроизведения музыки:
Теперь запустим метод в другом потоке. Сначала создаётся новый поток. Далее описывается объект Runnable в конструкторе потока. А внутри созданного потока вызываем наш метод play(). И, наконец, запускаем поток.
Усыпить поток
Иногда требуется временно приостановить поток («усыпить»):
Приоритет потоков
Для установки приоритета процесса используется метод setPriority(), который вызывается до запуска потока. Значение приоритета может варьироваться от Thread.MIN_PRIORITY (1) до Thread.MAX_PRIORITY (10):
Отмена выполнения потока
У потока есть метод stop(), но использовать его не рекомендуется, поскольку он оставляет приложение в неопределённом состоянии. Обычно используют такой подход:
Существует и другой способ, когда все запускаемые потоки объявляются демонами. В этом случае все запущенные потоки будут автоматически завершены при завершении основного потока приложения:
Источник
Многопоточность в Android. Все что вам нужно знать. Часть 1 — Введение
13.08.2017 в 11:40
Каждый Android разработчик, в тот или иной момент сталкивается с необходимостью иметь дело с потоками в своем приложении.
Когда приложение запускается, оно создает первый поток выполнения, известный как основной поток или main thread. Основной поток отвечает за отправку событий в соответствующие виджеты пользовательского интерфейса, а также связь с компонентами из набора инструментов Android UI.
Чтобы ваше приложение сохраняло отзывчивость, важно избегать использования основного потока для выполнения любой операции, которая может привести к его блокировке.
Сетевые операции и обращения к базе данных, а также загрузка определенных компонентов, являются типичными примерами операций, которые не следует выполнять в основном потоке. Когда они вызваны в главном потоке, они вызваны синхронно, что означает, что пользовательский интерфейс не будет ни на что реагировать до завершения операции. По этой причине, они обычно выполняются в отдельных потоках, что позволяет избежать блокировки пользовательского интерфейса во время выполнения (т. е. они выполняются асинхронно из UI).
Android предоставляет множество способов создания и управления потоками, и множество сторонних библиотек, которые делают управление потоками гораздо более приятным.
В этой статье вы узнаете о некоторых распространенных сценариях в Android разработке, где многопоточность становится важной, и некоторые простые решения, которые могут быть применены к этим сценариям, и многое другое.
Многозадачность в Android
В Android вы можете классифицировать все компоненты потоков на две основные категории:
Потоки связанные с активностью / фрагментом. Эти потоки привязаны к жизненному циклу активности / фрагмента и завершаются сразу после их уничтожения.
Потоки не связанные с активностью / фрагментом. Эти потоки могут продолжать работать за пределами жизни активности / фрагмента (если есть), из которых они были созданы.
Компоненты многопоточности, которые присоединяются к активности / фрагменту
AsyncTask
AsyncTask это наиболее основной Android компонент для организации потоков. Он прост в использовании и может быть хорошей основой для вашего сценария.
Однако, AsyncTask не подойдет, если вам нужен отложенный запуск задачи, после завершения работы вашей активности / фрагмента. Стоит отметить, что даже такая простая вещь, как вращение экрана может вызвать уничтожение активности.
Загрузчики
Загрузчики могут решить проблемы, упомянутые выше. Загрузчик автоматически останавливается, когда уничтожается активность и перезапускает себя, после пересоздания активности.
В основном есть два типа загрузчиков: AsyncTaskLoader и CursorLoader . О загрузчике CursorLoader вы узнаете далее в этой статье.
AsyncTaskLoader похож на AsyncTask , но немного сложнее.
Компоненты многопоточности, которые не присоединяются к активности / фрагменту
Service
Service это компонент, который полезен для выполнения длинных (или потенциально длительных) операций без какого-либо пользовательского интерфейса.
Service работает в основном потоке своего процесса; не создает свой собственный поток и не запускается в отдельном процессе, если вы это не указали.
Используя Service вы обязаны остановить его, когда его работа будет завершена, вызвав методы stopSelf() или stopService() .
IntentService
IntentService работает в отдельном потоке и автоматически останавливается после завершения работы.
IntentService обычно используется для коротких задач, которые не обязательно должны быть привязаны к какому-либо пользовательскому интерфейсу.
Источник
Многопоточность в Android. Looper, Handler, HandlerThread. Часть 1.
Что вы знаете о многопоточности в андроид? Вы скажете: «Я могу использовать AsynchTask для выполнения задач в бэкграунде». Отлично, это популярный ответ, но что ещё? «О, я слышал что-то о Handler’ах, и даже как то приходилось их использовать для вывода Toast’ов или для выполнения задач с задержкой…» — добавите Вы. Это уже гораздо лучше, и в этой статье мы рассмотрим как и для чего используют многопоточность в Android.
Для начала давайте взглянем на хорошо известный нам класс AsyncTask, я уверен что каждый андроид-разработчик использовал его. Прежде всего, стоит заметить, что есть отличное описание этого класса в официальной документации. Это хороший и удобный класс для управления задачами в фоне, он подойдёт для выполнения простых задач, если вы не хотите тратить впустую время на изучение того как можно эффективно управлять потоками в андроид. Самая главная вещь о которой вы должны знать – только метод doInBackground выполняется в другом потоке! Остальные его методы выполняются в главном UI потоке. Рассмотрим пример типичного использования AsyncTask:
Далее в интерфейсе будем использовать следующий тривиальный макет с прогресбаром для всех наших тестов.
В макете бесконечно отображается индикация вращения прогресбара. Если прогресбар застынет – это будет что в главном UI потоке происходит выполнение тяжелой работы.
Здесь мы используем AsyncTask, потому что приложению потребуется некоторое время для получения ответа от сервера и мы не хотим что бы нам интерфейс завис в ожидании этого ответа, поэтому мы поручаем выполнить сетевую задачу другому потоку. Есть много постов о том, почему использование AsyncTask – это плохая затея(например: если это внутренний класс вашего активити/фрагмента, то он будет удерживать внутреннюю ссылку на него, что является плохой практикой, потому что активити/фрагмент могут быть уничтожены при смене конфигурации, но они будут висеть в памяти пока работает фоновый поток; если объявлен отдельным статическим внутренним классом и вы используете ссылку на Context для обновления вьюшек, вы должны всегда проверять их на null).
Все задачи в главном потоке выполняются последовательно, делая тем самым код более предсказуемым – вы не рискуете попасть в ситуацию изменения данных несколькими потоками. Значит если какая-то задача работает слишком долго, Вы получите неотвечающее приложени, или ANR(Application Not Responding) ошибку. AsyncTask является одноразовым решением. Класс не может быть повторно использован при повторном вызове execute метода на одном экземпляре – вы непременно должны создать новый экземпляр AsyncTask для новой работы.
Любопытно то, что если вы попытаетесь показать Toast из метода doInBackground, то получите ошибку, содержащую что то вроде:
Из-за чего же мы получили ошибку? Ответ прост: потому что Toast является частью интерфейса и может быть показан только из UI потока, а правильный ответ: потому что он может быть показан только из потока с Looper’ом! Вы спросите, что такое Looper?
Хорошо, пришло время копнуть глубже. AsyncTask отличный класс, но что если его функциональности недостаточно для ваших действий? Если мы заглянем под капот AsyncTask, то обнаружим устройство с крепко связанными компонентами: Handler, Runnable и Thread. Каждый из вас знаком с потоками в Java, но в андройде вы обнаружите ещё один класс HandlerThread, произошедший от Thread. Единственное существенное отличие между HandlerThread и Thread заключается в том что первый содержит внутри себя Looper, Thread и MessageQueue. Looper трудится, обслуживая MessageQueue для текущего потока. MessageQueue это очередь которая содержит в себе задачи, называющиеся сообщениями, которые нужно обработать. Looper перемещается по этой очереди и отправляет сообщения в соответствующие обработчики для выполнения. Любой поток может иметь единственный уникальлный Looper, это ограничение достигается с помощью концепции ThreadLocal хранилища. Связка Looper+MessageQueue выглядит как конвейер с коробками. Задачи в очередь помещают Handler‘ы.
Вы можете спросить: «Для чего вся эта сложность, если задачи всё равно обрабатываются их создателями – Handler‘ами?». Мы получаем как минимум 2 преимущества:
— это помогает избежать состояния гонки (race conditions), когда работа приложения становится зависимой от порядка выполнения потоков;
— Thread не может быть повторно использован после завершения работы. А если поток работает с Looper’ом, то вам не нужно повторно создавать экземпляр Thread каждый раз для работы в бэкграунде.
Вы можете сами создавать и управлять Thread’ами и Looper’ами, но я рекомендую воспользоваться HandlerThread (Google решили использовать HandlerThread вместо LooperThread) : В нём уже есть встроенный Looper и всё настроено для работы.
А что о Handler? Это класс с двумя главными функциями: отправлять задачи в очередь сообщений (MessageQueue) и выполнять их. По умолчанию Handler неявно связывается с потоком в котором он был создан с помощью Looper’a, но вы можете связать его явно с другим потоком, если предоставите ему другой Looper в конструкторе. Наконец-то пришло время собрать все куски теории вместе и взглянуть на примере как всё это работает! Представим себе Activity в которой мы хотим отправлять задачи(в моей статье задачи представлены экземплярами Runnable интерфейса, что такое на самом деле задача(или сообщение) я расскажу во второй части статьи) в очередь сообщений(все активити и фрагменты существуют в главном UI потоке), но они должны выполняться с некоторой задержкой:
Так как mUiHandler связан с главным потоком(он получил Looper главного потока в конструкторе по умолчанию) и он является членом класса, мы можем получить к нему доступ из внутреннего анонимного класса и поэтому можем отправлять задачи-сообщения в главный поток. Мы используем Thread в примере выше и не можем использовать его повторно, если хотим выполнить новую задачу. Для этого нам придется создать новый экземпляр. Есть другое решение? Да! Мы можем использовать поток с Looper’ом. Давайте немного модифицируем пример выше с целью заменить Thread на HandlerThread для демонстрации того, какой прекрасной способностью к повторному использованию он обладает:
Я использую HandlerThread в этом примере, потому что я не хочу управлять Looper’ом сам, HandlerThread сам справится с этим. Один раз мы стартуем HandlerThread, после чего можем отправлять задачи в любое время, но помните о вызове метода quit когда вы хотите остановить HandlerThread. mWorkerHandler связан с MyWorkerThread с помощью его Looper. Вы не сможете инициализировать mWorkerHandler за пределами конструктора HandlerThread , так как getLooper будет возвращать null, потому что поток ещё не существует. Порой вам может встретится следующий способ инициализации Handler:
Порой это отлично работает, но иногда вы будете выхватывать NPE после вызова
postTask, с сообщением о том, что ваш mWorkerHandler — null. Сюрприз!
Почему это произошло? Хитрость здесь в нативном вызове, необходимом для создании нового потока. Если мы взглянем на кусок кода, где вызывается onLooperPrepared, мы найдём следующий фрагмент в HandlerThread классе:
Хитрость заключается в том, что run метод будет вызван только после того, как новый поток создаётся и запускается. И этот вызов может иногда произойти после вызова postTask(можете проверить это самостоятельно, просто поместите точку останова внутри postTask и onLooperPreparerd методов и взгляните, какой из них будет первым), таким образом вы можете стать жертвой состояния гонки между двух потоков (main и background).
Во второй части разберем как на самом деле внутри MessageQueue работают задачи.
Источник