Читать книгу Программирование приложений для мобильных устройств под управлением Android. Часть 2 - Евгений Владимирович Сенько, Евгений Сенько - Страница 4

Потоки

Оглавление

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

Для одновременно работающих программ и параллельно производимых вычислений используются Потоки – 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, который и выведет изображение на экран.

Программирование приложений для мобильных устройств под управлением Android. Часть 2

Подняться наверх