Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Многопоточность, синхронный и асинхронный код

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

Современные системы давно научились выполнять разные задачи в одно время. Мы можем одновременно слушать музыку, листать страницу в браузере и компилировать программу. Ваше приложение тоже может работать в подобном режиме. Функция может выполняться асинхронно, не мешая другому коду.

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

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

Конкурентность и параллелизм

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

Если конкурентность — общая способность кода делиться на части для одновременного выполнения, то параллелизм означает физическое выполнение нескольких задач одновременно на нескольких ядрах центрального процессора. Параллельные вычисления могут эффективно использовать современное многоядерное оборудование, что в некоторых случаях делает их более эффективными.

Main Thread

В стандартном приложении все действия, работающие с визуальными компонентами, выполняются в отдельном UI-потоке (Main thread, UI thread). Если в этот поток поместить «долгоиграющие» операции, например, загружать картинки с сайтов, пользователю постоянно будет казаться, что приложение зависает. Более того, приложение действительно может зависнуть и получить ошибку ANR (Application not responding), если приложение не отвечает более пяти секунд. Необходимо перенести подобные операции в отдельный поток, а в основном UI-потоке выводить индикатор выполнения задачи или конечный результат.

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

Главный поток обычно называют Main Thread или UI Thread. Оставляем его в покое. Разберёмся, что предлагают нам для решения проблемы. В разное время были актуальными разные подходы. Например, вот небольшой список (часть опущена).

  • AsyncTask (устарело)
  • Services/Службы
  • Jobs & JobSchedulers (тоже потихоньку устаревает)
  • WorkManager

Эти инструменты создавались под конкретные задачи и не являются универсальным решением.

NetworkOnMainThreadException

До версии Android 2.3 можно было писать код запроса на сервер в основном потоке. Я как раз начинал программировать под Android и немного застал это время. Вспомним старый пример (только код будет на Kotlin, которого тогда не было). Разместим на экране приложения кнопку для отправки запроса и ImageView, в манифесте добавим разрешение на работу с интернетом.


button.setOnClickListener {
    val catUrl = URL("http://developer.alexanderklimov.ru/android/images/android_cat.jpg")

    val httpConnection = catUrl.openConnection() as HttpURLConnection
    httpConnection.doInput = true
    httpConnection.connect()

    val inputStream = httpConnection.inputStream
    val bitmapImage = BitmapFactory.decodeStream(inputStream)

    imageView.setImageBitmap(bitmapImage)
}

Но с выходом версии 2.3 старый код перестал работать. Приложение падало с ошибкой NetworkOnMainThreadException. Строго говоря, уже тогда существовали рекомендации не использовать сетевые запросы в основном потоке. Профессионалы использовали Thread, Service, AsyncTask и приложения продолжали работать в новых версиях. Но множество примеров у начинающих программистов (у меня тоже) стали неработоспособными.

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


if (Build.VERSION.SDK_INT > 9) {
    val policy = ThreadPolicy.Builder().permitAll().build()
    StrictMode.setThreadPolicy(policy)
}

// Далее код из предыдущего примера

Трюк работает до сих пор, проверил сейчас на Android 6.0.

Thread и runOnUiThread

Kotlin на 100 % совместим с Java. Если требуется использовать потоки, как в Java, то можно воспользоваться функциями из стандартной библиотеки Kotlin. В частности, запустить новый поток можно с помощью функции thread(). Запустим новый поток и отобразим его название.


import kotlin.concurrent.thread

fun main() {
    println("I'm on ${Thread.currentThread().name}")

    thread {
        println("And I'm on ${Thread.currentThread().name}")
    }
}

I'm on main And I'm on Thread-0

Пример с ThreadPolicy был грязным лайфхаком, который никогда широко не использовался. Гораздо проще было создать отдельный поток и выполнять тяжёлую работу в нём. Но в созданном потоке нельзя обращаться напрямую к элементам интерфейса экрана, поэтому следовало добавить вызов runOnUiThread().

Перепишем предыдущий пример.


button.setOnClickListener {
    Thread {
        val catUrl =
            URL("http://developer.alexanderklimov.ru/android/images/android_cat.jpg")

        val httpConnection = catUrl.openConnection() as HttpURLConnection
        httpConnection.doInput = true
        httpConnection.connect()

        val inputStream = httpConnection.inputStream
        val bitmapImage = BitmapFactory.decodeStream(inputStream)

        runOnUiThread {
            imageView.setImageBitmap(bitmapImage)
        }
    }.start()
}

Можно избежать вызова runOnUiThread(), немного изменив код.


button.setOnClickListener {

    val mainLooper = Looper.getMainLooper()

    Thread {
        val catUrl =
            URL("http://developer.alexanderklimov.ru/android/images/android_cat.jpg")

        val httpConnection = catUrl.openConnection() as HttpURLConnection
        httpConnection.doInput = true
        httpConnection.connect()

        val inputStream = httpConnection.inputStream
        val bitmapImage = BitmapFactory.decodeStream(inputStream)

        Handler(mainLooper).post {
            imageView.setImageBitmap(bitmapImage)
        }
    }.start()
}

AsyncTask

Долгое время рекомендуемым способом работы с разных потоках был AsyncTask. На сайте до сих пор есть примеры с его использованием, например, Android: HttpURLConnection.

Несколько лет назад Гугл объявила и этот способ устаревшим.

Coroutines

В последнее время набирает популярность технология под названием Корутины/Coroutines. На сегодняшний день это самый рекомендуемый способ для создания асинхронного кода.



Реклама