[Разработка мобильных приложений, Разработка под Android] Чем опасен postDelayed
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Часто из-за особенностей работы android системы и sdk, нам необходимо подождать, когда определённая часть системы будет сконфигурирована или произойдёт какое-то необходимое нам событие. Зачастую это является костылём, но иногда без них никак, особенно в условиях дедлайнов. Поэтому во многих проектах для этого использовался postDelayed. Под катом рассмотрим, чем же он так опасен и что с этим делать.
Проблема
Для начала рассмотрим как обычно используют postDelayed():
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.postDelayed({
Log.d("test", "postDelayed")
// do action
}, 100)
}
С виду всё хорошо, но давайте изучим этот код повнимательнее:
1) Это отложенное действие, выполнение которого мы будем ожидать через некоторое время. Зная насколько динамично пользователь может совершать переходы между экранами, данное действие должно быть отменено при смене фрагмента. Однако, этого здесь не происходит, и наше действие выполнится, даже если текущий фрагмент будет уничтожен.
Проверить это просто. Создаём два фрагмента, при переходе на второй запускаем postDelayed с большим временем, к примеру 5000 мс. Сразу возвращаемся назад. И через некоторое время видим в логах, что действие не отменено.
2) Второе "вытекает" из первого. Если в данном runnable мы передадим ссылку на property нашего фрагмента, будет происходить утечка памяти, поскольку ссылка на runnable будет жить дольше, чем сам фрагмент.
3) Третье и основное почему я об этом задумался:
Падения приложения, если мы обращаемся ко view после onDestroyView
synthitec — java.lang.NullPointerException, поскольку кеш уже очищен при помощи _$_clearFindViewByIdCache, а findViewById отдаёт null
viewBinding — java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null
Что же делать?
1 Если нам нужные размеры view — использовать doOnLayout или doOnNextLayout
2 Перенести ожидание в компонент, ответственный за бизнес-логику отображения (Presenter/ViewModel или что-то другое). Он в свою очередь должен устанавливать значения во фрагмент в правильный момент его жизненного цикла или отменять действие.
3 Использовать безопасный стиль.
Необходимо отписываться от нашего действия перед тем, как view будет отсоединено от window.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Runnable {
// do action
}.let { runnable ->
view.postDelayed(runnable, 100)
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) {}
override fun onViewDetachedFromWindow(view: View) {
view.removeOnAttachStateChangeListener(this)
view.removeCallbacks(runnable)
}
})
}
}
Обычный doOnDetach нельзя использовать, поскольку view может быть ещё не прикреплено к window, как к примеру в onViewCreated. И тогда наше действие будет сразу же отменено.
Где то во View.kt:
inline fun View.doOnDetach(crossinline action: (view: View) -> Unit) {
if (!ViewCompat.isAttachedToWindow(this)) { // выполнится это условие
action(this) // и здесь мы сразу же отпишемся от действия
} else {
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) {}
override fun onViewDetachedFromWindow(view: View) {
removeOnAttachStateChangeListener(this)
action(view)
}
})
}
}
Или же обобщим в extension:
fun View.postDelayedSafe(delayMillis: Long, block: () -> Unit) {
val runnable = Runnable { block() }
postDelayed(runnable, delayMillis)
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) {}
override fun onViewDetachedFromWindow(view: View) {
removeOnAttachStateChangeListener(this)
view.removeCallbacks(runnable)
}
})
}
В принципе на этом можно остановится. Все проблемы решены. Но этим мы добавляем ещё один тип асинхронного выполнения к нашему проекту, что несколько усложняет его. Сейчас в мире Native Android есть 2 основных решения для асинхронного выполнения кода — Rx и Coroutines.
Попробуем использовать их.
Сразу оговорюсь, что не претендую на 100% правильность по отношению к вашему проекту. В вашем проекте это может быть по другому/лучше/короче.
Coroutines
Обычно во всех проектах есть базовый фрагмент для инжекта зависимостей, настройки di и так далее. Используем его для обобщения работы с отменой отложенных действий:
class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes), CoroutineScope by MainScope() {
override fun onDestroyView() {
super.onDestroyView()
coroutineContext[Job]?.cancelChildren()
}
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
Нам необходимо отменять все дочерние задачи в onDestroyView, но при этом не закрывать scope, поскольку после этого возможно вновь создание View без пересоздания Fragment. К примеру при роутинге вперёд на другой Fragment и после этого назад на текущий.
В onDestroy уже закрываем scope, так как далее никаких задач не должно быть запущено.
Все подготовительные работы сделаны.
Перейдём к самой замене postDelayed:
fun BaseFragment.delayActionSafe(delayMillis: Long, action: () -> Unit): Job? {
view ?: return null
return launch {
delay(delayMillis)
action()
}
}
Так как нам не нужно, чтобы наши отложенные задачи выполнялись, если view уже уничтожено, просто не запускаем ничего и возвращаем null. Иначе запускаем наше действие с необходимой задержкой. Но теперь при уничтожении view, задача будет отменена и ресурсы будут отпущены.
RX
В RX за отмену подписок отвечает класс Disposable, но в RX нет Structured concurrency в отличии от coroutine. Из-за этого приходится прописывать это всё самому. Выглядит обычно это примерно так:
interface DisposableHolder {
fun dispose()
fun addDisposable(disposable: Disposable)
}
class DisposableHolderImpl : DisposableHolder {
private val compositeDisposable = CompositeDisposable()
override fun addDisposable(disposable: Disposable) {
compositeDisposable.add(disposable)
}
override fun dispose() {
compositeDisposable.clear()
}
}
Также аналогично отменяем все задачи в базовом фрагменте:
class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes),
DisposableHolder by DisposableHolderImpl() {
override fun onDestroyView() {
super.onDestroyView()
dispose()
}
override fun onDestroy() {
super.onDestroy()
dispose()
}
}
И сам extension:
fun BaseFragment.delayActionSafe(delayMillis: Long, block: () -> Unit): Disposable? {
view ?: return null
return Completable.timer(delayMillis, TimeUnit.MILLISECONDS).subscribe {
block()
}.also {
addDisposable(it)
}
}
В заключении
При использовании каких-либо отложенных действий нельзя забывать, что это уже асинхронное исполнение, и соответственно оно требует отмены, в противном случае начинают происходить утечки памяти, краши и разные другие неожиданные вещи.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка мобильных приложений, Разработка под Android] Navigation Component-дзюцу, vol. 3 — Corner-кейсы
- [Разработка под iOS, Разработка игр, Разработка под Android, Unity, Дизайн игр] Alt: City Online. Как я в одиночку создавал «Gta Online» для мобильных устройств. Часть 1
- [Flutter, Разработка мобильных приложений] Flutter.dev: Continuous delivery с Flutter (перевод)
- [Gradle, Kotlin, Разработка мобильных приложений, Разработка под Android] Знакомство с App Gallery. Создаем аккаунт разработчика
- [Информационная безопасность, Разработка под Android, Разработка под Windows, Системы обмена сообщениями] Хакеры из Ирана годами воровали личные данные и прослушивали пользователей Telegram
- [Информационная безопасность, IT-инфраструктура, Исследования и прогнозы в IT, IT-компании] Зона доступа: 30 способов, которые позволят разблокировать любой смартфон. Часть 1
- [Unity, Разработка мобильных приложений, Финансы в IT] Компания-разработчик движка Unity успешно вышла на IPO
- [Разработка мобильных приложений, Разработка под Android, Разработка под iOS] Как мы автоматизировали разработку WL-приложений
- Уязвимость в Firefox для Android, позволяющая управлять браузером через общий Wi-Fi
- [Разработка под Android, Разработка мобильных приложений, Информационная безопасность] Google запретила в Play Store «сталкерские приложения» для слежки за супругами
Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_android, #_multithreading, #_rxandroid, #_coroutine, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_razrabotka_pod_android (
Разработка под Android
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:53
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Часто из-за особенностей работы android системы и sdk, нам необходимо подождать, когда определённая часть системы будет сконфигурирована или произойдёт какое-то необходимое нам событие. Зачастую это является костылём, но иногда без них никак, особенно в условиях дедлайнов. Поэтому во многих проектах для этого использовался postDelayed. Под катом рассмотрим, чем же он так опасен и что с этим делать. Проблема Для начала рассмотрим как обычно используют postDelayed(): override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) view.postDelayed({ Log.d("test", "postDelayed") // do action }, 100) } С виду всё хорошо, но давайте изучим этот код повнимательнее: 1) Это отложенное действие, выполнение которого мы будем ожидать через некоторое время. Зная насколько динамично пользователь может совершать переходы между экранами, данное действие должно быть отменено при смене фрагмента. Однако, этого здесь не происходит, и наше действие выполнится, даже если текущий фрагмент будет уничтожен. Проверить это просто. Создаём два фрагмента, при переходе на второй запускаем postDelayed с большим временем, к примеру 5000 мс. Сразу возвращаемся назад. И через некоторое время видим в логах, что действие не отменено. 2) Второе "вытекает" из первого. Если в данном runnable мы передадим ссылку на property нашего фрагмента, будет происходить утечка памяти, поскольку ссылка на runnable будет жить дольше, чем сам фрагмент. 3) Третье и основное почему я об этом задумался: Падения приложения, если мы обращаемся ко view после onDestroyView synthitec — java.lang.NullPointerException, поскольку кеш уже очищен при помощи _$_clearFindViewByIdCache, а findViewById отдаёт null viewBinding — java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null Что же делать? 1 Если нам нужные размеры view — использовать doOnLayout или doOnNextLayout 2 Перенести ожидание в компонент, ответственный за бизнес-логику отображения (Presenter/ViewModel или что-то другое). Он в свою очередь должен устанавливать значения во фрагмент в правильный момент его жизненного цикла или отменять действие. 3 Использовать безопасный стиль. Необходимо отписываться от нашего действия перед тем, как view будет отсоединено от window. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) Runnable { // do action }.let { runnable -> view.postDelayed(runnable, 100) view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(view: View) {} override fun onViewDetachedFromWindow(view: View) { view.removeOnAttachStateChangeListener(this) view.removeCallbacks(runnable) } }) } } Обычный doOnDetach нельзя использовать, поскольку view может быть ещё не прикреплено к window, как к примеру в onViewCreated. И тогда наше действие будет сразу же отменено. Где то во View.kt: inline fun View.doOnDetach(crossinline action: (view: View) -> Unit) {
if (!ViewCompat.isAttachedToWindow(this)) { // выполнится это условие action(this) // и здесь мы сразу же отпишемся от действия } else { addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(view: View) {} override fun onViewDetachedFromWindow(view: View) { removeOnAttachStateChangeListener(this) action(view) } }) } } Или же обобщим в extension: fun View.postDelayedSafe(delayMillis: Long, block: () -> Unit) {
val runnable = Runnable { block() } postDelayed(runnable, delayMillis) addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(view: View) {} override fun onViewDetachedFromWindow(view: View) { removeOnAttachStateChangeListener(this) view.removeCallbacks(runnable) } }) } В принципе на этом можно остановится. Все проблемы решены. Но этим мы добавляем ещё один тип асинхронного выполнения к нашему проекту, что несколько усложняет его. Сейчас в мире Native Android есть 2 основных решения для асинхронного выполнения кода — Rx и Coroutines. Попробуем использовать их. Сразу оговорюсь, что не претендую на 100% правильность по отношению к вашему проекту. В вашем проекте это может быть по другому/лучше/короче. Coroutines Обычно во всех проектах есть базовый фрагмент для инжекта зависимостей, настройки di и так далее. Используем его для обобщения работы с отменой отложенных действий: class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes), CoroutineScope by MainScope() {
override fun onDestroyView() { super.onDestroyView() coroutineContext[Job]?.cancelChildren() } override fun onDestroy() { super.onDestroy() cancel() } } Нам необходимо отменять все дочерние задачи в onDestroyView, но при этом не закрывать scope, поскольку после этого возможно вновь создание View без пересоздания Fragment. К примеру при роутинге вперёд на другой Fragment и после этого назад на текущий. В onDestroy уже закрываем scope, так как далее никаких задач не должно быть запущено. Все подготовительные работы сделаны. Перейдём к самой замене postDelayed: fun BaseFragment.delayActionSafe(delayMillis: Long, action: () -> Unit): Job? {
view ?: return null return launch { delay(delayMillis) action() } } Так как нам не нужно, чтобы наши отложенные задачи выполнялись, если view уже уничтожено, просто не запускаем ничего и возвращаем null. Иначе запускаем наше действие с необходимой задержкой. Но теперь при уничтожении view, задача будет отменена и ресурсы будут отпущены. RX В RX за отмену подписок отвечает класс Disposable, но в RX нет Structured concurrency в отличии от coroutine. Из-за этого приходится прописывать это всё самому. Выглядит обычно это примерно так: interface DisposableHolder {
fun dispose() fun addDisposable(disposable: Disposable) } class DisposableHolderImpl : DisposableHolder { private val compositeDisposable = CompositeDisposable() override fun addDisposable(disposable: Disposable) { compositeDisposable.add(disposable) } override fun dispose() { compositeDisposable.clear() } } Также аналогично отменяем все задачи в базовом фрагменте: class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes),
DisposableHolder by DisposableHolderImpl() { override fun onDestroyView() { super.onDestroyView() dispose() } override fun onDestroy() { super.onDestroy() dispose() } } И сам extension: fun BaseFragment.delayActionSafe(delayMillis: Long, block: () -> Unit): Disposable? {
view ?: return null return Completable.timer(delayMillis, TimeUnit.MILLISECONDS).subscribe { block() }.also { addDisposable(it) } } В заключении При использовании каких-либо отложенных действий нельзя забывать, что это уже асинхронное исполнение, и соответственно оно требует отмены, в противном случае начинают происходить утечки памяти, краши и разные другие неожиданные вещи. =========== Источник: habr.com =========== Похожие новости:
Разработка мобильных приложений ), #_razrabotka_pod_android ( Разработка под Android ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:53
Часовой пояс: UTC + 5