- Потоки
- Использование фоновых потоков
- Плохое приложение
- Запуск потока
- Усыпить поток
- Приоритет потоков
- Отмена выполнения потока
- Android UI thread
- Рассмотрим взаимодействие системы Android с компонентами приложения.
- Многопоточная разработка для Android, часть 1
- Содержание статьи
- Процессы и потоки
- Xakep #208. Атака на сигналку
- Потоки в Android
- Thread и Runnable
- Looper
- AsynkTask
- Сложность с отменой
- Потеря результатов
- Утечка памяти
- Что же делать?
- WeakReference
- Loaders
- To be continued
Потоки
Потоки позволяют выполнять несколько задач одновременно, не мешая друг другу, что даёт возможность эффективно использовать системные ресурсы. Потоки используются в тех случаях, когда одно долгоиграющее действие не должно мешать другим действиям. Например, у нас есть музыкальный проигрыватель с кнопками воспроизведения и паузы. Если вы нажимаете кнопку воспроизведения и у вас запускается музыкальный файл в отдельном потоке, то вы не можете нажать на кнопку паузы, пока файл не воспроизведётся полностью. С помощью потоков вы можете обойти данное ограничение.
Использование фоновых потоков
Чтобы быть уверенным, что ваше приложение не теряет отзывчивости, хорошим решением станет перемещение всех медленных, трудоёмких операций из главного потока приложения в дочерний.
Применение фоновых потоков — необходимое условие, если вы хотите избежать появления диалогового окна для принудительного закрытия приложения. Когда активность в 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 UI thread
Большая часть кода Android приложения работает в контексте компонент, таких как Activity, Service, ContentProvider или BroadcastReceiver. Рассмотрим, как в системе Android организованно взаимодействие этих компонент с потоками.
При запуске приложения система выполняет ряд операций: создаёт процесс ОС с именем, совпадающим с наименованием пакета приложения, присваивает созданному процессу уникальный идентификатор пользователя, который по сути является именем пользователя в ОС Linux. Затем система запускает Dalvik VM где создаётся главный поток приложения, называемый также «поток пользовательского интерфейса (UI thread)». В этом потоке выполняются все четыре компонента Android приложения: Activity, Service, ContentProvider, BroadcastReceiver. Выполнение кода в потоке пользовательского интерфейса организованно посредством «цикла обработки событий» и очереди сообщений.
Рассмотрим взаимодействие системы Android с компонентами приложения.
Activity. Когда пользователь выбирает пункт меню или нажимает на экранную кнопку, система оформит это действие как сообщение (Message) и поместит его в очередь потока пользовательского интерфейса (UI thread).
Service. Исходя из наименования, многие ошибочно полагают, что служба (Service) работает в отдельном потоке (Thread). На самом деле, служба работает так же, как Activity в потоке пользовательского интерфейса. При запуске локальной службы командой startService, новое сообщение помещается в очередь основного потока, который выпонит код сервиса.
BroadcastReceiver. При создании широковещательного сообщения система помещает его в очередь главного потока приложения. Главный поток позднее загрузит код BroadcastReceiver который зарегистрирован для данного типа сообщения, и начнёт его выполнение.
ContentProvider. Вызов локального ContentProvider немного отличается. ContentProvider также выполняется в основном потоке но его вызов является синхронным и для запуска кода ContentProvider не использует очередь сообщений.
Исходя из вышесказанного можно заметить, что если главный поток в данный момент обрабатывает пользовательский ввод или выполняет иное действие, выполнение кода, полученного в новом сообщении, начнётся только после завершения текущей операции. Если какая либо операция в одном из компонентов потребует значительного времени выполнения, пользователь столкнётся или с анимацией с рывками, или с неотзывчивыми элементами интерфейса или с сообщением системы «Приложение не отвечает» (ANR).
Для решения данной проблемы используется парадигма параллельного программирования. В Java для её реализации используется понятие потока выполнения (Thread).
Thread: поток, поток выполнения, иногда ещё упоминается как нить, можно рассматривать как отдельную задачу, в которой выполняется независимый набор инструкций. Если в вашей системе только один процессор то потоки выполняются поочередно (но быстрое переключение системы между ними создает впечатление параллельной или одновременной работы). На диаграмме показано приложение, которое имеет три потока выполнения:
Но, к сожалению, для взаимодействия с пользователем, от потока мало пользы. На самом деле, если вы внимательно посмотрите на диаграмму выше, вы поймёте, что как только поток выполнить все входящие в него инструкции он останавливается и перестаёт отслеживать действия пользователя. Чтобы избежать этого, нужно в наборе инструкций реализовать бесконечный цикл. Но возникает проблема как выполнить некое действие, например отобразить что-то на экране из другого потока, иными словами как вклиниться в бесконечный цикл. Для этого в Android можно использовать Android Message System. Она состоит из следующих частей:
Looper: который ещё иногда ещё называют «цикл обработки событий» используется для реализации бесконечного цикла который может получать задания используется. Класс Looper позволяет подготовить Thread для обработки повторяющихся действий. Такой Thread, как показано на рисунке ниже, часто называют Looper Thread. Главный поток Android на самом деле Looper Thread. Looper уникальны для каждого потока, это реализованно в виде шаблона проектирования TLS или Thread Local Storage (любопытные могут посмотреть на класс ThreadLocal в Java документации или Android).
Message: сообщение представляет собой контейнер для набора инструкций которые будут выполнены в другом потоке.
Handler: данный класс обеспечивает взаимодействие с Looper Thread. Именно с помощью Handler можно будет отправить Message с реализованным Runnable в Looper, которая будет выполнена (сразу или в заданное время) потоком с которым связан Handler. Код ниже иллюстрирует использование Handler. Этот код создаёт Activity которая завершиться через определённый период времени.
HandlerThread: написание кода потока реализующего Looper может оказаться не простой задачей, чтобы не повторять одни и те же ошибки система Android включает в себя класс HandlerThread. Вопреки названию этот класс не занимается связью Handler и Looper.
Практическую реализацию данного подхода можно изучить на примере кода класса IntentService, данный класс хорошо подходит для выполнения асинхронных сетевых или иных запросов, так как он может принимать задания одно за другим, не дожидаясь полной обработки текущего, и завершает свою работу самостоятельно.
Выполнение операций в отдельном потоке, не означает, что вы можете делать все что угодно, не влияя на производительность системы. Никогда не забывайте, что написанный вами код работает на машинах, как правило, не очень мощных. Поэтому всегда стоит использовать возможности предоставляемые системой для оптимизации.
Подготовлено на основе материалов AndroidDevBlog
Источник
Многопоточная разработка для Android, часть 1
Содержание статьи
При создании мобильного приложения чуть сложнее «Hello, world» почти наверняка требуется скачать что-то из Сети или считать файл с диска. Для стабильной работы программы в целом эти действия должны совершаться в отдельных потоках. Зачем, когда и как генерировать новые потоки в Android — об этом ты узнаешь в этой статье.
Процессы и потоки
Прежде чем разбираться с Android API, вспомним, какой структурой обладает эта ОС. В ее основе лежит Linux-ядро, в котором реализованы базовые механизмы, присущие всем *nix-системам. В ядре собраны модули, предназначенные для низкоуровневой работы: взаимодействия с железом, организации памяти, файловой системы и так далее.
В мире Linux каждая запущенная программа — это отдельный процесс. Каждый процесс обладает уникальным номером и собственной «территорией» — виртуальным адресным пространством, в рамках которого содержатся все данные процесса. Поток же — это набор инструкций внутри запущенной программы (процесса), который может быть выполнен отдельно. У потока нет своего уникального идентификатора и адресного пространства — все это он наследует от родительского процесса и делит с другими потоками.
Xakep #208. Атака на сигналку
Такое положение дел приводит к тому, что со стороны неизвестно, как протекает жизнь внутри процесса, есть ли там потоки и сколько их, — для ОС и других процессов это атомарная структура с уникальным идентификатором. Поэтому ОС может манипулировать лишь процессом, а управляет потоками только породивший их процесс. Вообще, внутренний мир операционных систем очень интересен, поэтому советую читателям почитать что-нибудь из классической литературы по Linux.
Когда в компьютерах (а вслед за ними — в планшетах и телефонах) появились процессоры с несколькими ядрами, программисты внедрили в ОС планировщик задач. Такой планировщик самостоятельно распределяет нагрузку по всем ядрам процессора, исполняя блоки кода параллельно или асинхронно, и тем самым повышает производительность. Поначалу маркетологи даже продавали компьютеры с лозунгом «Два ядра — в два раза быстрее», но, к сожалению, действительности он не соответствует.
В Android программист обязан повсеместно создавать новые потоки и процессы. Все операции, которые могут продлиться более нескольких секунд, должны обязательно выполняться в отдельных потоках. Иначе начнутся задержки в отрисовке интерфейса и пользователю будет казаться, что приложение «зависает».
Вообще, суть многопоточного программирования в том, чтобы максимально задействовать все ресурсы устройства, при этом синхронизируя результаты вычислений. Это не так легко, как может показаться на первый взгляд, но создатели Android добавили в API несколько полезных классов, которые сильно упростили жизнь Java-разработчику.
Потоки в Android
Запущенное в Android приложение имеет собственный процесс и как минимум один поток — так называемый главный поток (main thread). Если в приложении есть какие-либо визуальные элементы, то в этом потоке запускается объект класса Activity, отвечающий за отрисовку на дисплее интерфейса (user interface, UI).
В главном Activity должно быть как можно меньше вычислений, единственная его задача — отображать UI. Если главный поток будет занят подсчетом числа пи, то он потеряет связь с пользователем — пока число не досчиталось, Activity не сможет обрабатывать запросы пользователя и со стороны будет казаться, что приложение зависло. Если ожидание продлится чуть больше пары секунд, ОС Android это заметит и пользователь увидит сообщение ANR ( application not responding — «приложение не отвечает») с предложением принудительно завершить приложение.
Рис. 2. ANR-сообщение
Получить такое сообщение несложно — достаточно в главном потоке начать работу с файловой системой, сетью, криптографией и так далее. Как ты понимаешь, это очень плохая ситуация, которая не должна повторяться в штатных режимах работы приложения.
Thread и Runnable
Базовым классом для потоков в Android является класс Thread, в котором уже все готово для создания потока. Но для того, чтобы что-то выполнить внутри нового потока, нужно завернуть данные в объект класса Runnable. Thread, получив объект этого класса, сразу же выполнит метод run .
Но при такой организации сложно использовать всю силу дополнительных потоков — нельзя ни поменять задачу, ни поcмотреть результат вычислений. Хотя все это происходит в едином адресном пространстве, у Java-разработчика нет возможности просто так получить ресурсы соседних потоков.
Looper
Было бы классно уметь перекидывать данные из одного потока в другой. В Android, как и любой Linux-системе, это возможно. Один из доступных в Android способов — это создать очередь сообщений (MessageQueue) внутри потока. В такую очередь можно добавлять задания из других потоков, заданиями могут быть переменные, объекты или кусок кода для исполнения (Runnable).
Чтобы организовать очередь, нужно воспользоваться классами Handler и Looper: первый отвечает за организацию очереди, а второй в бесконечном цикле проверяет, нет ли в ней новых задач для потока.
Запуск такого потока устроен по похожей схеме — создание нового объекта и вызов метода start .
После выполнения этого метода создастся новый поток, который заживет своей жизнью. А это значит, что инициализация переменных и создание объектов будут уже идти параллельно с теми вызовами, которые забиты в следующих строчках после команды myLooper.start() . Поэтому перед обращением к очереди в новом потоке нужно немного подождать — объект handler может еще не существовать.
AsynkTask
Загружая или вычисляя что-то в фоне, хорошо бы не только получить результаты, но еще и иметь возможность выводить пользователю информацию о прогрессе. Конечно, все это можно сделать самому с помощью очереди сообщений, но разработчики Android и тут упростили нам жизнь, создав класс AsyncTask.
Класс AsyncTask — это представитель Java-обобщений (generic) в мире Android. У классов-обобщений заранее не определен тип данных, с которыми им предстоит работать. Этот прием позволяет создать класс, который в последующем сможет без проблем работать с любым типом данных. В данном случае благодаря дженерику AsynkTask возможно запускать новые потоки с совершенно произвольными объектами внутри.
C помощью AsyncTask теперь можно почти не думать (о вероятных последствиях позже) о создании потока, а просто создать объект и обрабатывать результаты. К примеру, с помощью AsyncTask удобно преобразовывать файлы (например, их зашифровать), при этом сам метод шифрования modifyFile может быть объявлен в Activity главного потока.
Помимо doInBackground , порождающего новый поток, в AsynkTask есть методы, которые будут выполняться уже в главном потоке.
Для запуска нового потока достаточно одной строчки в Activity:
С появлением AsyncTask разработчики обрели практически универсальный инструмент, позволяющий в короткие сроки написать код, генерирующий новые потоки, а потом так же быстро получить в UI результаты вычислений, отслеживая при этом прогресс. Но у этого класса есть несколько неприятных моментов, которые могут сделать приложение совершенно нестабильным.
Сложность с отменой
Для отмены вычислений существует метод cancel(boolean) , который в идеале должен остановить поток и высвободить ресурсы. Но этого не происходит. В случае если он вызван с аргументом false, вычисления будут продолжены, только их результат не будет возвращен в UI. Вызов cancel(true) лишь частично поможет решить проблему, поскольку существуют методы, которые из-за механизма синхронизации прервать нельзя, — к примеру, получение изображения с помощью BitmapFactory.decodeStream() .
Потеря результатов
Архитектура приложений построена таким образом, что главный поток может быть перезапущен в любой момент, — например, при перевороте устройства пользователем создается новый экземпляр Activity и в нем выполняется метод onCreate() . В этом случае у нового экземпляра Activity не будет связи с объектом AsyncTask, созданным и запущенным «старым» Activity. Поэтому все вычисления, которые не успели завершиться в AsyncTask до переворота устройства, будут потеряны.
Утечка памяти
А это самый неприятный недостаток AsyncTask, который напрямую следует из предыдущего пункта. После запуска нового Activity прошлый экземпляр UI должен быть выгружен из памяти сборщиком мусора. Но этого не произойдет, поскольку на «старый» UI есть ссылка в работающем потоке, созданном AsyncTask. Ничего не поделать, придется создавать еще один поток и запускать все вычисления в нем по новой. Но есть риск, что пользователь опять повернет экран! При такой организации рабочего процесса вся выделенная память потратится на содержание ненужных Activity и дополнительных потоков, и ОС принудительно завершит приложение с ошибкой OutOfMemoryException.
Что же делать?
Сделать экземпляр AsyncTask статическим и использовать слабые связи (WeakReference). При таком подходе в приложении не будут генериться лишние потоки, а слабая связность позволит сборщику мусора выгрузить ненужные Activity из памяти.
WeakReference
Немного о связях в Java. Создавая новый объект и ассоциируя его с переменной, мы создаем ссылку между ними. Привычное создание объекта сопровождается созданием сильной (strong) ссылки.
В Java нет принудительного уничтожения объектов, этим занимается сборщик мусора. Пока сильная ссылка существует, объект будет висеть в памяти. Разрушить такую ссылку можно только вручную, приравняв переменную sObj к null . Если же объект связан только слабыми ссылками (WeakReference), то сборщик мусора при первой же возможности выгрузит его из памяти.
В работающей программе неизвестно, в какой момент начнет свой очередной проход сборщик мусора. Поэтому лучше перестраховаться и каждый раз получать доступ к объекту по слабой ссылке.
Loaders
Пожалуй, основная задача 90% всех мобильных приложений вообще — это быстро и незаметно для пользователя загрузить данные из сети или файловой системы, а затем красиво отобразить их на дисплее. Для всего этого отлично подходит AsyncTask, но его проблемы не только неочевидны для неопытных разработчиков, но и плохо детектируются в процессе отладки.
Массовое распространение в Google Play приложений, имеющих проблемы с утечкой памяти, резонно вызовет у пользователей ощущение, что «Android тормозит». Компания Google решила взять ситуацию под свой контроль и добавила в API класс-загрузчик (Loader), который еще больше упрощает генерацию потоков и самостоятельно обходит слабые места AsyncTask. Создание нового потока теперь ведется через класс AsyncTaskLoader, который обязательно должен быть статическим.
Его реализация очень похожа на то, что приходится делать при использовании родительского AsyncTask, только теперь все вычисления в новом потоке находятся в методе loadInBackground .
Проблемы, которые вылезли в AsyncTask, решены путем введения промежуточного звена — менеджера загрузки. Класс LoaderManager.LoaderCallbacks управляет созданным потоком и по окончании вычислений возвращает данные в действующий Activity. Теперь достаточно быстро можно создать код, экономящий ресурсы и решающий проблему перезапуска Activity: вычисления продолжатся в самом первом потоке, а менеджер подключит новый Activity к ранее созданному потоку.
Для примера поместим на экран ListView, данные для которого поступят из сгенерированного потока. Менеджер потока тоже класс-дженерик, сегодня он будет работать со списком строк.
Теперь нужно создать менеджер и подключить к нему поток (Loader). Под управлением у менеджера может быть несколько потоков с уникальными номерами, но менеджер в Activity должен быть только один.
Данные будут обновляться после нажатия кнопки — к примеру, FloatingActionButton. Для доступа к менеджеру из обработчика setOnClickListener нужно добраться до контекста приложения и вытащить оттуда экземпляр класса LoaderManager.
Создаваться поток будет в методе onCreateLoader , который ОС вызовет и обработает самостоятельно. В качестве параметров этот метод принимает уникальный номер будущего потока (31337), а также объект класса Bundle, через который можно задавать параметры по связке «ключ — значение».
После того как AsyncTaskLoader выполнит все действия, в менеджере сработает метод onLoadFinished . Для передачи данных в UI тут нужно заново получить доступ к визуальным объектам, так как они могли быть пересозданы.
Чтобы избежать ошибок с передачей некорректных данных, нужно еще заполнить метод onLoaderReset . Он вызывается в том случае, если действия, выполняющиеся в AsyncTaskLoader, были отменены или перезапущены.
По каким-то причинам в загрузчике не был реализован аналог метода onProgressUpdate из AsyncTask. Но это возможно сделать самостоятельно, передавая данные в UI с помощью слабых ссылок.
To be continued
Сегодня мы разобрали особенности генерации потоков, которые могут быть не так очевидны, когда ты только-только начинаешь работать с Java или Android. Надеюсь, мир операционной системы от Google стал немножко понятней и у тебя появилось желание написать что-нибудь самому.
Тема потоков и процессов слишком большая, чтобы ее раскрыть в одной статье. Есть даже программисты, которых ценят именно за то, что они лучше всех умеют распараллеливать программу! Нам еще есть о чем поговорить — в стороне остались сервисы и процессы, поэтому в следующем номере мы продолжим разбираться с многопоточностью в Android. Пока почитай что-нибудь самостоятельно по теме, а если будут вопросы — пиши мне на почту. Удачи!
Источник