T.M. SoftStudio

feci quod potui, faciant meliora potentes

Купить полную версию курса "Программирование мобильных приложений для платформы Android" (русскоязычная версия, лекции, тесты, лабораторные работы)

Программирование мобильных приложений для платформы Android

Лекция 11. Потоки, асинхронные задачи и обработчики. Часть 1

Мобильные системы, как и все вычислительные устройства, сегодня, могут содержать несколько вычислительных ядер.

И это означает, что несколько программ или выполняемые потоки могут быть запущены на устройстве в одно и то же время, одновременно.

И это мощная вещь, потому что это позволяет вам делать больше работы за более короткий промежуток времени.

Но это также может сделать ваши программы намного более сложными, что приводит к ошибкам и проблемам с производительностью, если вы не осторожны.

Таким образом, в этом уроке мы собираемся говорить о написании многопоточных программ для Android.

И мы также собираемся обсудить некоторые из классов, которые Android предоставляет для поддержки этого.

В частности, в этом уроке я начну с краткого обсуждения потоков.

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

И я буду также обсуждать, как это влияет на дизайн вашего программного обеспечения.

После этого, я буду говорить о AsyncTask классе, который упрощает использование потоков в Android.

И, наконец, я завершу обсуждение классом обработчика, другого Android механизма многопоточности.

Итак, я использую термин поток, но что это поток?

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

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

Это изображение показывает эти понятия, и здесь я показываю гипотетическое вычислительное устройство.

Это устройство имеет два процессора – CPU 1 и CPU 2.

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

Теперь о процессоре два. Здесь я показываю два работающих процесса, P3 и P4.

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

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

И в одном из этих процессов, P4, я показываю два запущенные потоки, T7 и T8.

Теперь каждый из этих потоков является потоком последовательно выполняемых команд с собственным стеком вызовов.

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

В Java, потоки представлены объектом типа Thread в java.lang пакете.

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

Для более подробной информации о потоках можно посмотреть документацию по адресу:

Теперь некоторые из методов класса Thread, которые мы используем в этом уроке, включают метод start для запуска потока, и метод sleep для временной приостановки потока.

Некоторые методы объекта, которые могут понадобиться, когда вы используете потоки, включают метод wait, который позволяет заблокировать текущий поток и он будет ожидать, пока другой поток не вызовет соответствующий метод этого объекта, такой как notify(). И когда это произойдет, ожидающий поток может продолжить выполнение.

Метод notify() будит единственный поток, который ожидает в этом объекте.

Теперь, чтобы использовать поток, вы обычно делаете следующие вещи.

Во-первых, вы создаете поток. Например, с помощью оператора new.

Но, поток не запускается автоматически при его создании.

Чтобы запустить поток, необходимо вызвать метод start потока.

И это, в конечном итоге, приводит к вызову метода run потока, и поток продолжает выполняться до тех пор, пока метод run не завершится.

Таким образом, во-первых, работающее приложение выдает команду new для создания нового объекта потока.

Когда этот вызов завершается, приложение продолжает свою работу.

И спустя некоторое время, вызывает метод start потока.

И этот вызов возвращается обратно к приложению, но и запускает также код в методе run потока.

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

И, конечно, вы можете сделать это много раз, создавая и выполняя так много потоков, как вы захотите.

Итак, давайте посмотрим на приложение, в котором потоки были бы полезны.

Первое приложение, которое мы обсудим в этом уроке, называется ThreadingNoThreading.

И, как вы увидите, приложение отображает простой пользовательский интерфейс с двумя кнопками.

Первая кнопка называется Load Icon. И когда пользователь нажимает на эту кнопку, приложение открывает и читает файл, содержащий растровое изображение.

И как только это будет сделано, приложение показывает только что загруженное изображение на дисплее.

Идея заключается в том, что эта операция занимает заметное количество времени.

Теперь, в коде, который я использую, я собираюсь преувеличить это время.

Некоторые операции занимают относительно большое количество времени. И вы, как разработчик, должны понимать и иметь дело с этим.

Вторая кнопка называется Other Button, и когда пользователь нажимает эту кнопку, тост-сообщение всплывает и отображает некоторый текст.

А идея в том, что, если вы видите текст, то вы знаете, что кнопка работает.

Теперь, если вы не можете нажать на эту кнопку, или вы не видите текст, то что-то работает неправильно.

В частности, пользователь должен быть в состоянии нажать одну из этих кнопок в любое время, и система должна работать.

Итак, давайте запустим версию этого приложения, которое не использует потоки.

Теперь, что вы думаете, что произойдет?

Смогу ли я нажать обе кнопки всякий раз, когда я хочу?

Давайте посмотрим.

Здесь, я запущу ThreadingNoThreading приложение.

Как вы можете видеть, есть две кнопки, о которых мы говорили.

Я сначала нажму Other Button кнопку, и, как вы можете видеть, я могу нажать на нее, и обещанное сообщение появляется на дисплее.

Теперь я собираюсь сделать две вещи.

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

И сразу после того, как я нажимаю Load Icon кнопку, я собираюсь нажать Other Button кнопку снова.

Так, ну и что здесь происходит?

Other Button кнопка, кажется, залипла.

Почему это происходит?

Ну, ответ в том, что, когда я пытался нажать Other Button кнопку, Android еще загружал изображение, после того, как я нажал кнопку Load Icon.

И первая операция препятствовала выполнению второй операции.

Итак, одно, казалось бы, очевидное, но, в конечном счете, неверное решение этой проблемы, это пойти в код слушателя кнопки Load Icon и просто создать новый поток, который загружает изображение, а затем отображает его.

Я реализовал такой подход в приложении под названием ThreadingSimple.

Давайте взглянем на это приложение и поговорим о том, почему оно фактически не работает.

Итак, вот код ThreadingSimple приложения.

Вот слушатель кнопки Load Icon.

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

Давайте запустим этот код.

А теперь я нажимаю кнопку Load Icon, и пытаюсь нажать кнопку Other Button.

Все хорошо, кнопка Other Button отвечает и не блокируется кнопкой Load Icon.

Так что это хорошо, что мы добились определенного прогресса.

Тем не менее, вы можете увидеть, что у нас есть большая проблема сейчас.

Наше приложение упало.

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

Так что, Android просто не позволит потокам начать баловаться с представлением, которое было создано другим потоком.

Это означает, что в то время как новый поток, который мы создали, чтобы загрузить растровое изображение, может сделать эту работу, он не может на самом деле сделать последний шаг и добавить полученное растровое изображение на дисплей.

Так какой поток на самом деле создал эту иерархию представлений приложения?

Ну, все приложения для Android имеют главный поток, который также называется потоком пользовательского интерфейса.

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

Во всех этих методах жизненного цикла, о которых мы говорили оnCreate, оnStart, и т.д., все они обрабатываются в потоке пользовательского интерфейса.

И, кроме того, сам инструментарий пользовательского интерфейса UI toolkit не является потокобезопасным.

И все это означает, что если вы блокируете поток пользовательского интерфейса некоторой долгой операцией, то вы собираетесь блокировать ваше приложение от ответа на другие вещи, которые пользователь делает.

И мы на самом деле увидели это в ThreadingNoThreading приложении.

Так что, длительные операции должны быть помещены в фоновые потоки.

В то же время, однако, мы не можем получить доступ к UI toolkit из потока, который не является потоком пользовательского интерфейса.

И это мы имеет в приложении ThreadingSimple.

Итак, нам нужно сделать работу в фоновом потоке, но когда это работа сделана, нам нужно сделать обновление пользовательского интерфейса, обратно в потоке пользовательского интерфейса.

И Android, по сути, дает нам кучу способов сделать именно это.

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

Два из этих методов это методы:

Оба этих метода принимают параметр Runnable, который может, например, содержать код обновления дисплея.

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

Давайте посмотрим это в действии.

Итак, вот мое устройство и я запущу приложение ThreadingViewPost.

Тут также есть эти две кнопки.

И опять же, я собираюсь сделать две вещи сейчас.

Я нажму кнопку Load Icon. А потом сразу после этого, я нажму Other Button кнопку.

И я ожидаю увидеть, что Other Button кнопка не заблокирована Load Icon кнопкой.

И все так и работает.

Давайте взглянем на исходный код.



Я открою основную активность для этого приложения, и я пойду прямо к методу loadIcon, который вызывается, когда пользователь нажимает на кнопку Load Icon.

Как и прежде, этот код создает новый поток, а затем загружает изображение.

Но после загрузки изображения, вы видите, что теперь у нас есть вызов View.post, в который передается объект Runnable, чей код на самом деле вызывает метод setImageBitmap, чтобы установить только что загруженное изображение в представление.

Лекция 11. Потоки, асинхронные задачи и обработчики. Часть 2

Следующий класс поддержки потоков, который мы обсудим, это класс AsyncTask.

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

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

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

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

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

AsyncTask является параметризованным классом.

Он имеет три типа параметров Params, Progress, ResResultult.

Params является типом параметров, которые передаются выполняемой задаче.

Progress является типом промежуточного сообщения о ходе выполнения задачи.

Progress является типом результата, который вычисляется в результате фонового выполнения задачи.

Рабочий процесс асинхронного выполнения задачи начинается, во-первых, с вызова метода оnPreExecute, который выполняется в потоке пользовательского интерфейса до запуска метода doInBackground.

В методе оnPreExecute, как правило, устанавливается долго выполняемая операция.

После этого метод doInBackground выполняет основную часть работы в фоновом потоке.

И этот метод принимает список переменных как входные параметры и возвращает результат.

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

Если фоновый поток вызывает метод publishProgress, тогда в потоке пользовательского интерфейса вызывается метод onProgressUpdate, при условии, что фоновый поток по-прежнему работает.

И, наконец, метод оnPostExecute вызывается в потоке пользовательского интерфейса после окончания фоновых вычислений, результат которых передается методу в качестве параметра.

Давайте посмотрим на версию нашего приложения, загружающего иконку,

реализованного с помощью класса AsyncTask.

Запустим приложение ThreadingAsyncTask.

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

Итак, вот я нажимаю кнопку Load Icon, и вы можете видеть небольшой прогресс бар, который появляется и медленно заполняется.

Теперь я нажму кнопку Other Button, и мы видим знакомый всплывающий текст.

И наконец, появляется изображение.

Давайте посмотрим на исходный код этого приложения.

Откроем файл основной активности и посмотрим на слушателя кнопки загрузки изображения.

Этот код создает новый экземпляр задачи загрузки изображения, а затем вызывает метод выполнения задачи, передавая идентификатор ресурса изображения в качестве параметра.

Итак, давайте взглянем на класс LoadIconTask подробнее.

LoadIconTask расширяет класс AsyncTask является задачей асинхронный и его типы параметров это целые для параметров, целые для прогресса, и bitmap для результата.

Первый метод, на который мы будем смотреть, это метод оnPreExecute.

Этот метод выполняется в потоке пользовательского интерфейса, и его целью является сделать индикатор видимым на дисплее.

Следующий метод это метод doInBackground.

Этот метод получает целое, как параметр.

Это целое число является идентификатором ресурса для изображения, который был передан методу execute задачи LoadIconTask.

Метод doInBbackground выполняет работу по загрузке изображения.

Когда он делает это, он периодически вызывает метод publishProgress, передавая целое число, которое представляет собой процент выполненной загрузки.

И опять же, этот пример немного надуманный, чтобы упростить ситуацию.

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

Следующий метод, это метод onProgressUpdate.

Этот метод работает в потоке пользовательского интерфейса, получает целое число, которое было передано в метод publishProgress, а затем устанавливает индикатор чтобы отразить процент проделанной работы.

И, наконец, последний метод это метод оnPostExecute.

Этот метод, опять же, работает в потоке пользовательского интерфейса, и он получает только что загруженное изображение в качестве параметра.

Этот метод первым делом делает индикатор невидимым, так как в нем уже нет необходимости, а затем устанавливает загруженное изображение в View-представление изображения.

Последнее, о чем я хочу поговорить в этом уроке это класс Handler-обработчика.

Как и задачи AsyncTask, класс Handler-обработчика обеспечивает передачу работы между двумя потоками.

Класс Handler-обработчика является более гибким, по сравнению с классом AsyncTask, так как он может работать с любыми двумя потоками, а не только с фоновым потоком и потоком пользовательского интерфейса.

Handler-обработчик связывается с определенным потоком.

Один поток может передавать работу другому потоку, посылая сообщения или путем размещения Runnable-объектов для Handler-обработчика, который связан с другим потоком.

Итак, сначала давайте обсудим сообщения и Runnable-объекты, а затем мы углубимся в архитектуру самого Handler-обработчика.

Вы уже знаете о Runnable-объектах.

Вы можете использовать их, когда поток-отправитель точно знает, какую работу он хочет выполнить, но он хочет, чтобы эта работа была выполнена в потоке Handler-обработчика.

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

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

Итак, теперь давайте поговорим о том, как Handler-обработчики используют эти Message-сообщения и Runnable-объекты.

Каждый Android-поток связан с объектом MessageQueue и объектом Looper.

MessageQueue это структура данных, которая содержит Message-сообщения и Runnable-объекты.

Looper принимает эти Message-сообщения и Runnable-объекты из MessageQueue и отправляет их по мере необходимости.

На рисунке поток А создает Runnable-объект.

И при этом он использует объект Handler-обработчика для размещения Runnable-объекта в поток Handler-обработчика.

Когда поток А делает это, Runnable-объект помещается в объект очереди MessageQueue потока, связанного с Handler-обработчиком.

Теперь что-то очень похожее происходит с Message-сообщениями.

На этом рисунке поток B создает сообщение, и при этом он использует Handler-обработчик, его метод sendMessage, чтобы отправить это сообщение потоку Handler-обработчика.

Когда поток В делает это, сообщение размещается в очереди MessageQueue, связанной с Handler-обработчиком.

Теперь, когда все это происходит, Looper объект находится там и ждет работу, которая появляется в MessageQueue.

И когда эта работа действительно появляется, Looper реагирует одним из двух способов, в зависимости от вида работы, которая только что прибыла.

Теперь, если работа представляет собой сообщение, Looper будет обрабатывать сообщение, вызывая метод handleMessage Handler-обработчика передавая ему сообщение самостоятельно.

Если вместо этого, работа является Runnable-объектом, тогда Looper будет обрабатывать его, просто вызывая метод run Runnable-объекта.

Теперь, вот некоторые из методов, которые вы можете использовать при размещении Runnable-объектов для Handler-обработчика.

Мы уже видели метод post, хотя есть ряд других методов, которые позволяют вам планировать работу для исполнения в разное время.

Например, вы можете использовать метод postAtTime для добавления Runnable-объекта в MessageQueue, но для запуска его в определенное время.

Существует также метод postDelayed, и он позволяет добавлять Runnable-объект в MessageQueue, но запустить его по истечении заданного времени задержки.

Если же вы хотите отправлять сообщения, сначала необходимо создать сообщение.

Один из способов сделать это, состоит в использовании метода оbtainMessage Handler-обработчика, который возвращает сообщение с уже установленным Handler-обработчиком.

Вы также можете использовать метод obtain класса Message.

И как только у вас появилоссь сообщение, вы захотите установить данные для сообщения.

Есть ряд вариантов для этого, указанный в документации.

Как и для Runnable-объектов, существует ряд методов, которые можно использовать для отправки сообщения.

Существует метод sendMessage, о котором мы только что говорили.

Есть также метод, который позволяет поставить сообщение в верхнюю часть очереди MessageQueue, чтобы обработать сообщение как можно скорее.

Есть метод sendMessageAtTime для отправки сообщения в очередь в определенное время.

Есть также метод sendMessageDelayed, который ставит в очередь сообщение с указанной задержкой к текущему моменту времени.

Давайте посмотрим на исходный код нашего примера, который был реализован с использованием Handler-обработчика.

Откроем ThreadingHandlerRunnable приложение и его главную активность.

И вы видите, что этот код создания нового обработчика.

Этот обработчик создается в главном потоке пользовательского интерфейса.

Так что Runnable-объект, который будет получать Handler-обработчик, будет выполняться в потоке пользовательского интерфейса.

Здесь также есть слушатель для кнопки загрузки изображения.

Когда пользователь нажимает на эту кнопку, этот код создает и запускает новый поток, чей run метод определяется задачей загрузки изображения.

Давайте посмотрим на этот класс.

Этот метод run начинается, размещая новый Runnable-объект, который при выполнении делает прогресс-бар видимым.

Далее он загружает изображение, при этом периодически публикуя свой прогресс, размещая другой Runnable-объект, который вызывает метод setProgress для индикатора.

Затем он отправляет Runnable-объект, который устанавливает загруженное изображение на дисплее.

И он заканчивает, размещая Runnable-объект, который делает прогресс-бар невидимым.

Давайте также рассмотрим другую версию этого приложения, в котором отправляются сообщения, вместо размещения Runnable-объектов.

Откроем ThreadingHandlerMessages приложение и его главную активность.

И вы видите код создания нового обработчика.

И опять же, этот обработчик создается в основном потоке пользовательского интерфейса.

Работа, которую этот обработчик выполняет, будет выполнена в потоке пользовательского интерфейса.

Как вы можете видеть, этот обработчик имеет метод handleMessage, в котором он реализует различные виды работ.

Этот метод начинается с проверки кода сообщения, который находится в сообщении.

И потом, он выполняет соответствующие действия для этого кода сообщения.

Например, если код set_progress_bar_visibility, устанавливается статус видимости для прогресс-бара.

Если код progress_update, устанавливается состояние хода выполнения индикатора выполнения.

Если код set_bitmap, устанавливается изображение на дисплее.

Теперь давайте вернемся к слушателю кнопки загрузки изображения.

Тут, как и раньше, когда пользователь нажимает кнопку загрузки изображения, этот код создает и запускает новый поток, метод run которого определяется задачей загрузки изображения.

И этот run метод начинается с получения сообщения с кодом, установленным как set_progress_bar_visibility, и с аргументом, указывающим, что индикатор выполнения должны быть сделан видимым.

Затем он отправляет это сообщение в Handler-обработчик, который будет обрабатывать его сделает индикатор видимым.

Далее загружается изображение.

И пока оно загружается, периодически публикуется прогресс, путем получения и отправки сообщения с кодом progress_update, и с аргументом, который указывает процент выполнения работы.

Это приводит к вызову обработчиком метода setProgress для индикатора выполнения.

Затем получается и посылается сообщение, чтобы установить загруженное изображение на дисплее.

И, наконец, посылается последнее сообщение, чтобы сделать индикатор невидимым.