Освой Android играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Мы все пишем последовательный код и его выполнение идёт строчка за строчкой в соответствии с вашей задумкой. Когда мы вызываем функцию, которая выполняет длительную операцию, то должны дождаться её окончания и затем можем продолжить следующую строчку кода.
Современные системы давно научились выполнять разные задачи в одно время. Мы можем одновременно слушать музыку, листать страницу в браузере и компилировать программу. Ваше приложение тоже может работать в подобном режиме. Функция может выполняться асинхронно, не мешая другому коду.
Многопоточность позволяет выполнять несколько задач одновременно в рамках одного процесса. Это позволяет эффективно использовать вычислительные ресурсы мобильного устройства и повышать производительность приложения.
Самые очевидные примеры, когда длительные операции мешают нам, это запрос в интернет, извлечение данных из базы данных, файловые операции. Даже если операции длятся несколько секунд или всего полсекунды, по компьютерным меркам это вечность. А если таких операций много, то приложение становится неудобным в пользовании. Соответственно, эти задачи следует выполнять в отдельном фоновом потоке.
В стандартном приложении все действия, работающие с визуальными компонентами, выполняются в отдельном UI-потоке (Main thread, UI thread). Если в этот поток поместить «долгоиграющие» операции, например, загружать картинки с сайтов, пользователю постоянно будет казаться, что приложение зависает. Более того, приложение действительно может зависнуть и получить ошибку ANR (Application not responding), если приложение не отвечает более пяти секунд. Необходимо перенести подобные операции в отдельный поток, а в основном UI-потоке выводить индикатор выполнения задачи или конечный результат.
Ваша задача - не засорять основной поток. Все тяжёлые операции следует перенести в фоновый поток. Тогда кнопки будут нажиматься, картинки обновляться, текст набираться, список прокручиваться и т.д.
Главный поток обычно называют Main Thread или UI Thread. Оставляем его в покое. Разберёмся, что предлагают нам для решения проблемы. В разное время были актуальными разные подходы. Например, вот небольшой список (часть опущена).
Эти инструменты создавались под конкретные задачи и не являются универсальным решением.
До версии 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.
Пример с 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. На сайте до сих пор есть примеры с его использованием, например, Android: HttpURLConnection.
Несколько лет назад Гугл объявила и этот способ устаревшим.
В последнее время набирает популярность технология под названием Корутины/Coroutines. На сегодняшний день это самый рекомендуемый способ для создания асинхронного кода.