1. Потоки в Android. Создание рабочих потоков.

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

MainActivity.java
public Bitmap getBitmapFromURL(String src) {
    try {
        Bitmap myBitmap = null;
        for (int i = 0; i < 1; i++) {
            URL url = new URL(src);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setDoInput(true);
            connection.connect();
            InputStream input = connection.getInputStream();
            myBitmap = BitmapFactory.decodeStream(input);
        }

        return myBitmap;
    } catch (IOException e) {
        // Log exception
        return null;
    }
}

Также мы должны указать в файле манифеста разрешение использовать интернет

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="ua.opu.pnit.threadex">

    <uses-permission android:name="android.permission.INTERNET"/>
    
    ...

</manifest>

Создадим макет с ImageView и кнопкой. По нажатию на кнопку мы загружаем изображение из интернета и показываем его в ImageView.

Казалось бы, задача простая, но если мы запустим приложение и нажмем на кнопку, то заметим, что наше приложение ведет себя странно – оно «зависает» и не реагирует на действия пользователя. После нескольких секунд устройство сообщает, что приложение не отвечает и предлагает его закрыть (такое состояние называется ANR Application Not Responding).

Почему приложение ведет себя таким образом? Попробуем разобраться и исправить ситуацию.

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

Процесс – это совокупность кода и данных, которая имеет общее виртуальное адресное пространство (подробнее про процессы читайте здесь и здесь). По умолчанию все компоненты одного приложения работают в одном процессе и вы, в 99% случаях не должны менять это поведение. Помимо процесса, операционная система создает главный поток процесса.

Поток (Thread) – это единица исполнения кода. Каждый поток последовательно выполняет инструкции процесса, которому он принадлежит. Кроме основного потока, в процессе могут быть дополнительные потоки, которые работают параллельно с главным.

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

Более подробно работу потоков можно изучить с помощью профайлера (View -> Tool Windows -> Profiler). Перейдем во вкладку CPU и выберем наше запущенное приложение.

При запуске приложения система создает поток выполнения для приложения, который называется «главным» (его иногда называют main, UI Thread и так далее). Этот поток очень важен, так как он отвечает за диспетчеризацию событий элементов графического интерфейса. Он также является потоком, в котором приложение взаимодействует с компонентами из набора инструментов пользовательского интерфейса Android (компонентами из пакетов android.widget и android.view). По существу, главный поток — это то, что иногда называют потоком пользовательского интерфейса.

Выполнение долговременных операций в основном потоке, таких как сетевой доступ или работа с файлами или базой данных, приведет к блокированию всего пользовательского интерфейса. Когда поток заблокирован, не могут обрабатываться никакие события, включая события изменения отображения. С точки зрения пользователя приложение выглядит зависшим. Хуже того, если поток пользовательского интерфейса заблокирован более нескольких секунд (в настоящее время около 5 секунд), отображается печально известное диалоговое окно «приложение не отвечает».

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

Чтобы решить данную проблему, нам следует запустить свой отдельный поток (такие потоки называют worker thread или рабочими потоками), в рамках которого необходимо произвести загрузку изображения и вывод его на экран.

Для работы с потоками в Java существует класс Thread. Он содержит весь необходимый функционал для создания и запуска потока, но ему недостает одного – он не знает, какой код ему необходимо выполнить внутри себя. Таким образом, мы должны передать ему какой-то код для исполнения, который нужно выполнить в потоке.

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

MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        ...

        findViewById(R.id.button).setOnClickListener(v -> {

            new Thread(() -> {
                Bitmap image = getBitmapFromURL("https://i.pinimg.com/originals/8e/97/d1/8e97d100673bfb291a8801734c899bda.png");
                ImageView imgview = findViewById(R.id.image);
                imgview.setImageBitmap(image);
            }).start();

        });
    }

    public Bitmap getBitmapFromURL(String src) {...}
}

Запустим приложение и попробуем загрузить изображение. К сожалению, наше приложение закрылось с ошибкой. Посмотрим логи и попробуем разобраться в причине ошибки

Process: ua.opu.pnit.threadex, PID: 14513
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7753)

Итак, что означает сообщение "Only the original thread that created a view hierarchy can touch its views" ?

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

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

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

Обратиться из рабочего потока к основному можно несколькими способами:

  1. использовать метод Activity.runOnUiThread(Runnable action) – как можно понять из названия, данный метод запускает объект типа Runnable в UI потоке;

  2. вызвать метод View.post(Runnable) у UI объекта окна. Он запускает объект Runnable в UI потоке;

  3. вызвать метод View.postDelayed(Runnable action, long delayMillis) у UI объекта окна. Он запускает объект Runnable в UI потоке через delayMillis миллисекунд.

Также вы можете использовать стандартные инструменты Java из пакета concurrency (например, Callable и FutureTask).

Модифицируем один из методов создания потока. Загрузим картинку, а потом обратимся к главному потоку и установим изображение.

MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        findViewById(R.id.button).setOnClickListener(v -> {        
            new Thread(() -> {
                Bitmap image = getBitmapFromURL("https://i.pinimg.com/originals/8e/97/d1/8e97d100673bfb291a8801734c899bda.png");

                MainActivity.this.runOnUiThread(() -> {
                    ImageView imgview = findViewById(R.id.image);
                    imgview.setImageBitmap(image);
                });

            }).start();
        });
    }

    public Bitmap getBitmapFromURL(String src) {
        try {
            Bitmap myBitmap = null;
            for (int i = 0; i < 1; i++) {
                URL url = new URL(src);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setDoInput(true);
                connection.connect();
                InputStream input = connection.getInputStream();
                myBitmap = BitmapFactory.decodeStream(input);
            }

            return myBitmap;
        } catch (IOException e) {
            // Log exception
            return null;
        }
    }
}

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

Last updated