Пишите на Java и ждёте асинхронные вызовы API прямо во фрагменте или Activity? Используя анонимные классы? В этой статье я расскажу, как Kotlin позволяет это сделать без вреда для GC и возможных IllegalStateException.
В данной статье приведёна работа со слабыми ссылками на примере ожидания асинхронных вызовов из компонентов Android приложения. Однако данный опыт применим и для других ситуаций, когда требуется использовать слабые ссылки.
PS. Я уже довольно давно пишу на Swift. А еще раньше писал Android приложения на Java 6. И желания возвращаться к ней у меня не возникало ни на секунду. Но по долгу службы мне все же потребовалось разработать Android приложение. К тому моменту компания JetBrains уже сделала релиз jvm-компилируемого языка Kotlin (в момент написания статьи — версии 1.1.1). Посмотрев документацию по нему, я твердо решил, что мой проект будет не на Java.
Сначала я приведу пример обработки асинхронных вызовов с использованием Java — стандартного инструмента для разработки под Android.
Java (<8)
Рассмотрим стандартная ситуация, когда вы прототипируете приложение и делаете запросы прямо из компонента UI (в данном случае, Activity):
// MainActivity.java
void loadComments() {
api.getComments(new Api.ApiHandler<Api.Comment>() {
@Override
public void onResult(@Nullable List<Api.Comment> comments, @Nullable Exception exception) {
if (comments != null) {
updateUI(comments);
} else {
displayError(exception);
}
}
});
}
Криминал в данном случае очевиден. Анонимный класс хендлера держит сильную ссылку на компонент (неявное свойство this$0 в дебаггере), что не очень хорошо, если пользователь решит завершить Activity.
Решить данную проблему можно, если использовать слабую ссылку на наше Activity:
// MainActivity.java
void loadCommentsWithWeakReferenceToThis() {
final WeakReference<MainActivity> weakThis = new WeakReference<>(this);
api.getComments(new Api.SimpleApiHandler<Api.Comment>() {
@Override
public void onResult(@Nullable List<Api.Comment> comments, @Nullable Exception exception) {
MainActivity strongThis = weakThis.get();
if (strongThis != null)
if (comments != null)
strongThis.updateUI(comments);
else
strongThis.displayError(exception);
}
});
}
Конечно, это не сработает. Как упоминалось ранее, анонимный класс держит сильную ссылку на объект, в котором был создан.
Единственным решением остается передавать слабую ссылку (или создавать внутри) в другой объект, который не подвержен жизненному циклу компонента (в нашем случае объект класса Api):
// MainActivity.java
public class MainActivity extends AppCompatActivity implements Api.ApiHandler<Api.Comment> {
void loadCommentsWithWeakApi() {
api.getCommentsWeak(this);
}
@Override
public void onResult(@Nullable List<Api.Comment> comments, @Nullable Exception exception) {
if (comments != null)
updateUI(comments);
else
displayError(exception);
}
// Api.java
class Api {
void getCommentsWeak(ApiHandler<Comment> handler) {
final WeakReference<ApiHandler<Comment>> weakHandler = new WeakReference<>(handler);
new Thread(new Runnable() {
@Override
public void run() {
… // getting comments
ApiHandler<Comment> strongHandler = weakHandler.get();
if (strongHandler != null) {
strongHandler.onResult(new ArrayList<Comment>(), null);
}
}
}).start();
}
…
}
В итоге мы совсем избавились от анонимного класса, наше Activity теперь реализует интерфейс хендлера Api и получает результат в отдельный метод. Громоздко. Не функционально. Но зато больше нет удержания ссылки на Activity.
Как бы я сделал в Swift:
// ViewController.swift
func loadComments() {
api.getComments {[weak self] comments, error in // слабый захват self
guard let `self` = self else { return } // если self нет, то выходим
if let comments = comments {
self.updateUI(comments)
} else {
self.displayError(error)
}
}
}
В данном случае объект за идентификатором self
(значение примерно такое же, как this
в Java) передается в лямбду как слабая ссылка.
И на Pure Java мне такое поведение вряд ли удасться реализовать.
Kotlin
Перепишем наш функционал на Kotlin:
// MainActivity.kt
fun loadComments() {
api.getComments { list, exception ->
if (list != null) {
updateUI(list)
} else {
displayError(exception!!)
}
}
}
Лямбды в Kotlin (как и в Java 8) более умные, чем анонимные классы, и захватывают в себя аргументы только если они используются в нем самом. К сожалению, нельзя указать правила захвата (как в C++ или в Swift), поэтому ссылка на Activity захватывается как сильная:
(тут можно заметить, как лямбда является объектом, реализующем интерфейс Function2<T,V>
)
Однако что нам мешает передавать слабую ссылку в лямбду:
// MainActivity.kt
fun loadCommentsWeak() {
val thisRef = WeakReference(this) // слабая ссылка на Activity
api.getComments { list, exception ->
val `this` = thisRef.get() // получаем Activity или null
if (`this` != null)
if (list != null) {
`this`.updateUI(list)
} else {
`this`.displayError(exception!!)
}
}
}
Как видно из дебаггера, у нашего хендлера больше нет прямой ссылки на Activity, что и требовалось добиться. У нас получился безопасный обработчик ответа асинхронного вызова, написанный в функциональном стиле.
Однако сахар Kotlin позволит мне еще больше приблизится к синтаксису Swift:
// MainActivity.kt
fun loadCommentsWithMagic() {
val weakThis by weak(this) // искусственная weak-переменная
api.getComments { list, exception ->
val `this` = weakThis?.let { it } ?: return@getComments
if (list != null)
`this`.updateUI(list)
else
`this`.displayError(exception!!)
}
}
Конструкция val A by B
является назначением переменной A объект-делегат B, через которого будут устанавливаться и получаться значение переменной A.
weak(this)
— упрощенная функция-конструктор специального класса WeakRef
:
// WeakRef.kt
class WeakRef<T>(obj: T? = null): ReadWriteProperty<Any?, T?> {
private var wref : WeakReference<T>?
init {
this.wref = obj?.let { WeakReference(it) }
}
override fun getValue(thisRef:Any? , property: KProperty<*>): T? {
return wref?.get()
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
wref = value?.let { WeakReference(it) }
}
}
// Та самая функция-конструктор
fun <T> weak(obj: T? = null) = WeakRef(obj)
WeakRef
является декоратором WeakReference, позволяющего использовать его как делегат. Подробнее про делегировние в Kotlin можно прочитать на сайте языка.
Теперь конструкция val A by weak(B)
даёт возможность декларировать слабые переменные и свойства. В Swift, например, данная фича это поддерживается на уровне языка:
weak var A = B
Добавим еще сахарку
// MainActivity.kt
fun loadCommentsWithSugar() {
val weakThis by weak(this)
api.getComments { list, exception -> weakThis?.run {
if (list != null)
updateUI(list)
else
displayError(exception!!)
}}
}
В определенной части кода мы начинаем вызывать функции нашего Activity даже без указания какого-то конкретного объекта, как будто ссылаемся на наше исходное activity, что автоматически захватывает его в хендлер. А мы от этого так долго пытались избавится.
Как видно из дебаггера, этого не происходит.
Замечательное свойство лямбд в Kotlin — возможность устанавливать его владельца (как в Javascript). Таким образом this
в лямбде после weakThis?.run
принимает значение объекта Activity, причем сама лямбда выполнится только тогда, когда данный объект еще находится в памяти. Функция run()
является расширением любого типа и позволяет создать лямбду с владельцем объекта, у которого оно вызвано (Еще есть другие магические функции вроде let()
, apply()
, also()
).
В дебаггере владелец лямбды указывается как свойство $receiver
.
Подробнее про лямбды в Kotlin можно найти на сайте языка.
Напоследок еще немного сахара:
// MainActivity.kt
fun loadCommentsWithDoubleSugar() = this.weak().run {
// здесь this уже WeakReference<Activity>
api.getComments { list, exception -> this.get()?.run {
// здесь this уже Activity
if (list != null)
updateUI(list)
else
displayError(exception!!)
}}}
// weakref.kt
// добавляем во все классы функцию weak()
fun <T>T.weak() = WeakReference(this)
Update: Java 8
Лямбды в Java 8 также не захватывают объект, где были созданы:
void loadCommentsWithLambdaAndWeakReferenceToThis() {
final WeakReference<MainActivity> weakThis = new WeakReference<>(this);
api.getComments((comments, exception) -> {
MainActivity strongThis = weakThis.get();
if (strongThis != null)
if (comments != null)
strongThis.updateUI(comments);
else
strongThis.displayError(exception);
});
}
У Android пока еще нет полной поддержки Java 8, но некоторые фичи уже поддерживаются. До Android Studio 2.4 Preview 4 потребуется использовать Jack toolchain.
Выводы
В данной статье я привёл пример того, как с помощью Kotlin можно решить проблему безопасного ожидания асинхронных вызовов из копонентов жизненного цикла приложения Android, а так же сравнил его с решением, которое предлагает Java (<8).
Kotlin позволил написать код функциональном стиле без ущерба безопасности для жизненного цикла, что, несомненно, является плюсом.
> Для ознакомления со всеми фичами языка можно почитать документацию.
> Как интергрировать Kotlin в Android проект можно узнать здесь.
> Исходники проекта на git.
Update: Добавил про Java 8
Комментарии (11)
Jukobob
08.04.2017 10:51+1У меня возникло несколько вопросов:
1) А причем тут Kotlin?
-Обертки вокруг сильных ссылок появились в Java с 1.2 версии и вы просто сделали «те же яйца но на котлиновских референсах». Просто тема уже изжеванная.
2) Зачем пытаться изобрести велосипед и изначально выбирать не правильное решение по способу загрузки. Простейший MVP решит все ваши проблемы с уничтожением View.
3) Если я не ошибаюсь, то очистка WeakRef происходит с вызовом GC.
val thisRef = WeakReference(this) // слабая ссылка на Activity api.getComments { list, exception -> val `this` = thisRef.get() // получаем Activity или null
Что то может пойти не так…
4) Extention functions
fun <T>T.weak() = WeakReference(this)
Предположим у меня небольшой проект с очень качественным single resposibility. Как скажется данный подход на вашем DexCount?
btw, я не являюсь адептом java или kotlin. Использую и то и другое в продакшене. Последний проект был целиком написан на kotlin. Но все же это просто инструмент (с более низким порогом вхождения после swift).dante_photo
08.04.2017 15:25+11) Возможно, стоило озаглавить "… в Java и Kotlin". Конечно, получилось вроде "безопасная обработке асинхронных вызовов в той же части кода, в которой этот вызов был совершен", но это немного жирно для заголовка. Да и тут представлен именно подход.
2) MVP — это еще одно равноправное решение данной проблемы. В своем проекте я использую Clean Architecture (VIP). Очень удобно декларировать слабую ссылку того же презентора на view через делегирование:
var view by weak<View>()
В Swift я бы написал
weak var view: View?
3)
thisRef
в лямбду захватывается сильной ссылкой. Очистится ли сама ссылка? Cложно поверить, что GC очистит слабую ссылку на валидный объект.
4) По-хорошему количество новых методов соответствовать количеству разных классов, на объектах которого вызывается данный метод.
все же это просто инструмент
Согласен.
voddan
08.04.2017 21:26+2Удивлен что не было упомянуто решение "в Kotlin стиле" — завернуть все эту логику с ссылками в одну функцию:
api.getCommentsWeakRef { list, exception -> if (list != null) updateUI(list) // the methods are called on a weak reference else displayError(exception!!) }
Nakosika
09.04.2017 17:04+1Уже два года как люди используют RxJava. Отписываешься в onDestroy и порядок. Да, разбиение на маленькие классы помогает от утечек памяти в том числе.
Yoto
09.04.2017 17:27+1К теме статьи относится мало, но вдруг Вам будет интересно.
От Jack'а отказались.
fogone
09.04.2017 18:00+2С weak-ом такое вот решение еще можно, если уже котлин использовать:
class WeakContext<T>(self: T) { private val reference = WeakReference(self) val self: T? get() = reference.get() inline fun <R> ifSelfDefined(body: T.()->R): R? = self?.let(body) } inline fun <T, R> T.weakable(body: WeakContext<T>.()->R): R = WeakContext(this).body() class Test { fun someMethod() = weakable { api.getComments { list, exception -> ifSelfDefined<Unit> { if (list != null) updateUI(list) else displayError(exception!!) } } } }
fogone
10.04.2017 09:15+1понял, что контекст не нужен, т. е. еще проще:
inline fun <T, R> T.weakable(body: WeakReference<T>.() -> R): R = WeakReference(this).body() inline fun <T, R> WeakReference<T>.ifSelfDefined(body: T.() -> R): R? = get()?.let(body) class Test { fun someMethod() = weakable { api.getComments { list, exception -> ifSelfDefined { if (list != null) updateUI(list) else displayError(exception!!) } } } }
и что важно, совершенно бесплатная абстракция выходит, как если бы мы сохраняли ссылку перед вызовом
andreich
17.04.2017 14:39Подход хороший, только вот пример совсем неудачный, не надо так работать с активити :)
apro
Да, другой класс должен быть не anonymous, но разве его нельзя сделать
static
inner
?А если использовать lambda из Java 8, последние версии android studio вроде имеют
неплохую поддержку java 8?
dante_photo
Смысл в том, чтобы обрабатывать асинхронные вызовы в одном месте кода. Не вижу решения проблемы через static inner (или просто static) классы.
Поизучал лямбды в Java 8. Они действительно ведут себя лучше, чем анонимные классы, т.е. захватывают контекст только по потребности. Обновлю статью.