Потоки
Мобильные устройства, как и компьютеры, сегодня все чаще содержат по несколько вычислительных ядер. Это означает, что несколько программ могут работать на этих устройствах одновременно. И это – мощная вещь, потому что она позволяет производить больше вычислений за меньшее время. Но это также сделает ваши программы намного более сложными, что может привести к ошибкам и проблемам производительности, если вы, как разработчик, не будете внимательны и осторожны.
Для одновременно работающих программ и параллельно производимых вычислений используются Потоки – Threads. Концептуально поток – одно из многих возможных вычислений, работающих одновременно в операционной системе. С точки зрения реализации каждый поток имеет свой собственный счетчик команд и стек времени исполнения, но совместно использует «кучу» (структура данных, с помощью которой реализована динамически распределяемая память приложения) и области статического ЗУ с другими работающими потоками.
Диаграмма, изображающая эти понятия.
Здесь показано гипотетическое вычислительное устройство. У этого устройства есть два процессора, CPU 1 и CPU 2. Каждый из этих процессоров может выполнять инструкции приложений, работающих на устройстве. Далее на CPU 2 показаны два выполняющихся процесса – p3 и p4. Один из вариантов – рассматривать процессы как автономные среды исполнения. У них есть ресурсы, такие как память, открытые файлы, сетевые соединения и другие вещи, которыми они управляют и разделяют с другими процессами на устройстве. И в одном из этих процессов, p4, показаны два работающих потока – т7 и т8. Каждый из этих потоков – последовательно выполняющийся ряд инструкций со своим собственным стеком вызовов. Но так как они работают в одном и том же процессе, каждый из них может получить доступ к совместно используемым ресурсам этого процесса, включая статические переменные.
В Java потоки представлены объектом типа thread в пакете java.lang. Потоки в Java реализуют интерфейс runnable. Это означает, что у них должен быть открытый (public) метод называющийся run, который не получает параметров и у него нет возвращаемого значения. Среди методов потоки имеют метод start для запуска потока и метод sleep для того, чтобы временно приостановить выполнение потока. Метод wait заставляет поток приостановиться и ждать пока другой поток не разбудит его методом notify.
Чтобы использовать поток, во-первых, необходимо создать поток. Например, при помощи команды new. Далее, потоки автоматически не запускаются, чтобы запустить поток необходимо вызвать метод start. И, в конечном счете происходит вызов метода run, который будет выполняться до тех пор, пока не будет остановлен.
Работающее приложение дает команду new, чтобы создать объект – новый поток. Далее работа приложения продолжается, и когда-то позже произойдет вызов метода запуска потока start. При этом произойдет возврат к приложению, но параллельно начнет выполняться код в методе потока run. И, поскольку программа продолжается, то теперь одновременно выполняются два потока. Таким образом, проделав это многократно, можно создать и выполнять столько потоков, сколько необходимо.
Рассмотрим приложение, в котором поточная обработка была бы полезна, ThreadingNoThreading. Приложение выводит на экран простой пользовательский интерфейс с двумя кнопками. Первая кнопка маркирована «LoadIcon». Когда пользователь кликает по этой кнопке, приложение открывает и читает файл, содержащий изображение, и затем показывает его на дисплее. Идея здесь состоит в том, что эта работа может занимать значительное количество времени. В данном примере количество времени несколько преувеличено для наглядности. Некоторые операции занимают относительно большое количество времени. И разработчик должен это понимать и правильно манипулировать этим.
Вторая кнопка маркирована «Other Button». Когда пользователь кликает по этой кнопке, раскрывается тост-сообщение, выводя на экран некоторый текст. А здесь идея в том, что, если вы видите текст, значит кнопка работает. Соответственно, если вы не можете нажать кнопку или не видите текст, значит что-то работает неправильно.
В идеале пользователь должен быть в состоянии нажать любую из кнопок в любое время и получить реакцию, система просто должна работать. Давайте запустим версию этого приложения, которое не использует поточной обработки. Нажмем кнопку «Other Button», и на дисплее появляется обещанное сообщение.
А теперь поступим иначе. Сначала нажмем кнопку «Load Icon», которая запустит трудоемкую работу чтения битового массива из файла и вывода изображения на экран. И сразу же после нажатия кнопки «Load Icon», не дожидаясь появления картинки, нажмем кнопку «Other Button». И что же тогда произойдет? Кажется кнопка «Other Button» не работает. Почему? Потому что при попытке нажать «Other Button», Android все еще загружал картинку после нажатия кнопки «Load Icon» и это препятствовало тому, чтобы вторая кнопка сработала.
У этой проблемы есть одно на вид очевидное, но, в конечном счете, неправильное решение – перейти к «слушателю» кнопки «Load Icon» и просто создать новый поток, который будет загружать битовый массив и затем выведет его на экран. Пример такой реализации – приложение Threadingsimple, вот его код.
«Слушатель» кнопки «Load Icon» вызывает метод loadIcon (), который показан чуть ниже. Этот код создает новый поток, который занимает время, загружая битовый массив. И затем пытается установить изображение в image view, который является частью лейаута.
Теперь, если запустить приложение и нажать кнопку «Load Icon», а затем сразу нажать «Other Button», видно, что есть реакция на «Other Button» и её нажатие не блокируется. Это хорошо, есть прогресс. Однако появилась большая проблема, через несколько секунд приложение рухнуло.
Если исследовать журнал logcat, мы увидим, что есть сообщение о том, что только тот поток, в котором создана иерархия вьюшек, может манипулировать своими вьюшками.
Это означает, что новый поток, который мы создали, чтобы загрузить битовый массив, может сделать эту работу, но он не сможет сделать последний шаг и вывести получившееся изображение на дисплей.
И так, какой же поток на самом деле создал иерархию вьюшек этого приложения? У всех Android приложений есть основной поток, который также называют UI thread – потоком пользовательского интерфейса. Все компоненты приложения, которые работают в одном и том же процессе, запущенном по умолчанию, используют тот же поток – UI thread. Все методы жизненного цикла: OnCreate, OnStart и т.д., работают в основном потоке. И, кроме того, сам инструментарий пользовательского интерфейса не ориентирован на многопотоковое исполнение. Все это означает, что, если вы заблокируете UI-поток любой продолжительной работой, это воспрепятствует реакции приложения на другие манипуляции пользователя. Следовательно, долго выполняющиеся операции должны быть помещены в фоновые потоки. Однако, вместе с этим, мы не сможем получить доступ к инструментарию пользовательского интерфейса от потока, не принадлежащего UI tread. Таким образом, мы должны делать длительную работу в фоновом потоке, но когда эта работа сделана, мы должны сделать обновление пользовательского интерфейса в потоке UI.
И Android на самом деле дает нам набор средств сделать это. В частности Android обеспечивает несколько методов, которые гарантированно будут работать в потоке пользовательского интерфейса. Два из них – это метод post класса View и runOnUiThread класса Activity. Оба этих метода получают параметр Runnable, содержащий код, который, например, обновляет экран в наших недавних примерах. Таким образом, если мы используем эти методы, мы могли бы загрузить битовый массив в фоновом потоке, и когда эта операция завершится, мы использовали бы один из этих методов, чтобы выполнить Runnable, который и выведет изображение на экран.
Давайте рассмотрим, как это реализуется в коде. Откроем основную Activity этого приложения и перейдем прямо к методу loadIcon, который вызывается, когда пользователь нажимает кнопку «Load Icon». Как и в предыдущем примере, этот код создает новый поток и затем загружает битовый массив. Но после загрузки данных, теперь происходит вызов view.post, запускающий новый Runnable, код которого и вызывает метод setImageBitmap, чтобы установить загруженное изображение в соответствующий imageView.
Следующий класс, обеспечивающий поточную обработку, это класс AsyncTask. Он обеспечивает правильное и простое использование потока пользовательского интерфейса. Этот класс служит общей основой для управления задачами, которые, как в наших предыдущих примерах, включают операции, которые должны выполняться в фоновом потоке, а их результат публикуется в UI потоке. Основой использования AsyncTask является то, что работа разделена между фоновым потоком и потоком UI. Фоновый поток выполняет длительную операцию и может дополнительно сообщать о ее прогрессе. Поток пользовательского интерфейса, с другой стороны, ответственен за начальную настройку длительно выполняемой операции, за публикацию промежуточной информации о прогрессе и за завершение операции после того, как фоновый поток сделал свою работу.
AsyncTask – универсальный класс. Он определяется тремя основными параметрами: params, progress, и result. Params – тип параметров, которые отправляются в AsyncTask при выполнении. Progress – тип любых единиц прогресса, публикуемого в процессе выполнения фоновой операции. Result – тип результата фонового вычисления.
Во время выполнения AsyncTask проходит через четыре шага. Во-первых, onPreExecute () – метод вызывается в потоке UI до начала работы фонового потока.
Сразу же после него запускается doInBackground (Params…) и выполняет вычисления в фоновом режиме. Сюда же передаются параметры. И этот же метод возвращает в последний шаг результат типа Result.
Во время работы doInBackground может дополнительно вызвать метод publishProgress (Progress…) с перечнем переменных, который обеспечит некоторую индикацию прогресса длительного процесса. А переданные переменные используются в UI потоке при помощи метода onProgressUpdate (Progress…), который запускается в потоке пользовательского интерфейса после вызова publishProgress.
И последний шаг, onPostExecute (Result) вызывается в UI потоке после окончания фоновых вычислений с результатом, возвращенным как его параметр.
Давайте рассмотрим версию нашего приложения загрузки картинки, реализованного с AsyncTask. Если мы запустим приложение, то увидим, что оно выглядит подобно предыдущим примерам, но здесь добавлен новый элемент интерфейса – индикатор выполнения, который показывает, какое количество работы по загрузке изображения уже выполнено.
После нажатия кнопки «Load Icon» появится маленький индикатор выполнения и начнет медленно заполняться. После нажатия второй кнопки раскроется знакомый текст – кнопка «Other Button» работает. И наконец, на экране появится изображение.
А теперь рассмотрим исходный код этого приложения. Откроем основную Activity.
«Слушатель» кнопки «Load Button» создает новый экземпляр LoadIconTask и сразу же происходит вызов execute с передачей в качестве параметра id ресурса изображения.
Далее рассмотрим класс LoadIconTask более подробно. LoadIconTask – это AsyncTask с параметрами: params – типа целое, progress – типа целое и result – типа битовый массив.
Первый метод, который мы рассмотрим, это onPreExecute. Этот метод выполняется в UI потоке и его назначение – сделать индикатор выполнения видимым на дисплее.
Следующий метод – doInBackground. Этот метод получает в качестве параметра целое число – id ресурса для битового массива, который был передан в метод LoadiconTask. execute. doInBackground выполняет работу загрузки битового массива. И пока он делает это, то периодически вызывает publishProgress, передавая параметр, представляющий собой процент загрузки, которая была проделана до этого момента. Этот пример является немного надуманным с целью упрощения понимания.
Следующий метод – onProgressUpdate. Этот метод выполняется в потоке пользовательского интерфейса, получает параметр, который был передан в PublishProgress, и затем устанавливает индикатор выполнения для отображения процента выполненной работы.
И наконец, последний метод – onPostExecute. Этот метод так же работает в UI потоке и получает только что загруженный массив (Bitmap) в качестве параметра.
Сначала onPostExecute делает индикатор выполнения невидимым, так как он больше не нужен и затем выводит загруженное изображение на экран.
Класс Handler – это обработчик, позволяющий отправлять и обрабатывать сообщения и объекты Runnable, ассоциированные с очередью сообщений потока. Каждый его экземпляр связан с отдельным потоком и его очередью сообщений. Как и AsyncTask, класс Handler предназначен для взаимодействия между двумя потоками, но он более гибок, так как может работать с любыми двумя потоками, а не только между фоновым и потоком пользовательского интерфейса. Каждый поток может передать работу другому потоку путем отправки сообщения или объекта Runnable обработчику, который с ним ассоциирован.
Мы используем объекты Runnable, когда отправитель (поток-заказчик) точно знает какую работу и как необходимо выполнить, но выполнить её надо в потоке обработчика. С другой стороны, Сообщение – это класс, который может содержать данные, такие как код сообщения, произвольный объект данных или целочисленные аргументы. И мы будем использовать сообщения, когда поток-отправитель указывает какую операцию необходимо выполнить в другом потоке, а как её выполнить определит handler.
В Android каждый поток связан с очередью сообщений – messageQueue и петлителем – Looper. Очередь сообщений – структура данных, которая содержит сообщения и объекты runnable. Петлитель забирает эти сообщения и объекты runnable из очереди сообщений и диспетчеризирует их в зависимости от обстоятельств.
Эта диаграмма изображает поток Tread A, который создает Runnable внутри метода post (опубликовать) и использует объект-обработчик – Handler, чтобы отправить его в поток этого обработчика. Runnable помещается в очередь сообщений потока, связанного с обработчиком. И тоже самое происходит с сообщениями.
А эта диаграмма изображает поток Thread B, который создает сообщение и использует метод обработчика sendMessage, чтобы отправить это сообщение в поток обработчика. Сообщение помещается в очередь сообщений, связанную с этим обработчиком.
В то время, когда выполняются все вышеописанные операции, объект-петлитель просто «сидит» ожидая работы, которая появится в очереди сообщений. И когда эта работа появляется, петлитель реагирует одним из двух способов в зависимости от вида работы, которая только что появилась. Если эта работа – сообщение, петлитель обрабатывает это сообщение, вызывая метод обработчика handleMessage и передавая в нем само сообщение. Если же эта работа будет Runnable, то петлитель просто вызывает его метод run.
Существует несколько методов, которые позволяют запланировать выполнение работы в разное время. Например, можно использовать метод postAtTime, чтобы добавить runnable в очередь сообщений и выполнить его в определенное вами время. Метод postDelayed позволяет добавить runnable в очередь сообщений и выполнить его после указанной задержки.
Если вы хотите отправлять сообщения, сначала необходимо сообщение создать. Один из способов это сделать – использовать метод обработчика obtainMessage, который выдаст вам сообщение уже с установленным обработчиком. Можно также использовать метод obtain класса message.
Что касается runnable, то есть несколько методов, которые можно использовать, чтобы отправить сообщение. Метод sendMessage уже упоминался. Есть также его версия, которая позволяет поместить сообщение вперед в очереди сообщений, чтобы оно было выполнено как можно скорее. Есть метод sendMessageAtTime, чтобы поставить сообщение в очередь согласно требуемому времени. Есть также метод sendMessageDelayed, который поставит сообщение в очередь в текущее время плюс указанная задержка.
Давайте рассмотрим исходный код некоторых версий нашего рабочего примера, в котором были использованы обработчики. Вот приложение Threading handler runnable. Откроем основную Activity этого приложения. Во-первых, вы видите, что этот код создает новый обработчик. Этот обработчик создается в основном потоке пользовательского интерфейса. Таким образом, объекты runnable, которые получает этот обработчик, будут выполняться в UI потоке.
Далее вы видите «слушателя» кнопки «Load Icon». Когда пользователь нажимает кнопку «Load Icon», этот код создает и запускает новый поток, метод run которой определен runnable-объектом LoadIconTask.
Метод run класса LoadIconTask начинается, создавая новый объект Runnable, который при исполнении сделает видимой шкалу прогресса.
Далее происходит загрузка битового массива. И во время этой загрузки, периодически публикуется её прогресс методом setProgress другого runnable.
Далее создается новый Runnable, который устанавливает только что загруженный битовый массив на экран. И заканчивается все, созданием последнего Runnable, который делает шкалу прогресса невидимой.
А теперь рассмотрим вторую версию этого приложения, которое отправляет сообщения вместо runnable – ThreadingHandlerMessages. Откроем основную Activity этого приложения. Во-первых, этот код создает новый обработчик. И как в предыдущем примере, этот обработчик будет создаваться основным UI потоком. И соответственно работа, которую выполняет этот обработчик, будет выполняться в UI потоке. У этого обработчика есть метод handleMessage, в котором он реализует различные виды работ. Этот метод вначале проверяет код сообщения, содержащийся в этом сообщении, и затем выполняет действие, определенное для этого кода сообщения.
Например, если код – set_progress_bar_visibility, то будет установлено состояние видимости шкалы прогресса. Если код – progress_update, то будет установлено значение прогресса на шкале прогресса. Если код – set_bitmap, то этот код выводит битовый массив на дисплей.
Теперь перейдем к «слушателю» кнопки «Load Icon». Как и в предыдущих примерах, когда пользователь нажимает кнопку, этот код, создает и запускает новый поток, чей метод run определен runnable-объектом LoadIconTask.
Выполнение этого метода начинается при получении сообщения с кодом set_progress_bar_visibility с параметром, указывающим, что шкала прогресса должна стать видимой. Тогда это сообщение отправляется в обработчик, который и сделает шкалу прогресса видимой.
Далее происходит загрузка битового массива. И одновременно с этим, периодически обновляется прогресс, при помощи сообщения с кодом progress_update и с параметром, который указывает процент выполненной работы. Это будет результатом вызова обработчиком метода setProgress объекта progressBar.
Затем принимается и отправляется сообщение, чтобы вывести только что загруженное изображение на дисплей. И наконец, отправляется последнее сообщение, чтобы сделать шкалу прогресса невидимой.