RecyclerView и DiffUtil, утечки памяти и ANR
Финальный блок собеседования — про то, почему приложение тормозит, течёт и падает. Здесь ценят конкретику: инструменты, цифры, реальные истории.
ANR (Application Not Responding) — системный диалог «приложение не отвечает». Возникает, когда главный поток заблокирован дольше порога: примерно 5 секунд для реакции на ввод, 10 секунд для широковещательного приёмника на переднем плане, 20 секунд для сервиса.
Вопрос 1: «Как работает RecyclerView и зачем DiffUtil?»
Что проверяет интервьюер
Понимаете ли вы, что RecyclerView экономит не память под данные, а создание View — самую дорогую операцию в списке.
Развёрнутый ответ
RecyclerView держит пул переиспользуемых ViewHolder. Когда элемент уезжает за границу экрана, его holder не уничтожается, а возвращается в пул и затем привязывается к новым данным через onBindViewHolder. Инфляция разметки (onCreateViewHolder) происходит лишь для нескольких элементов, помещающихся на экран, плюс небольшой запас.
Отсюда важное практическое следствие: onBindViewHolder обязан быть дешёвым. Он вызывается на каждый кадр скролла. Загрузка изображений, форматирование дат, создание новых объектов и слушателей там — прямая дорога к рывкам (jank).
notifyDataSetChanged() сообщает «изменилось всё»: RecyclerView перепривязывает все видимые элементы и не может показать анимации вставки, удаления и перемещения. DiffUtil вычисляет минимальный набор изменений между старым и новым списком, а ListAdapter делает это в фоновом потоке:
class ArticleAdapter :
ListAdapter<Article, ArticleViewHolder>(DIFF) {
companion object {
private val DIFF = object : DiffUtil.ItemCallback<Article>() {
// тот же ли это объект по смыслу
override fun areItemsTheSame(old: Article, new: Article) = old.id == new.id
// изменилось ли содержимое (для data class достаточно ==)
override fun areContentsTheSame(old: Article, new: Article) = old == new
}
}
}
// обновление списка: адаптер сам посчитает разницу в фоне
adapter.submitList(newArticles)
Два уточняющих вопроса, которые задают следом:
- Почему список нужно передавать новый, а не мутировать старый? DiffUtil сравнивает две ссылки. Если изменить тот же самый список, старая и новая версии совпадут, и разницы не найдётся — список «не обновится».
- Что такое payload? Третий метод
getChangePayloadпозволяет сказать «изменилось только количество лайков» и перерисовать одно поле вместо всего holder.
Вопрос 2: «Откуда берутся утечки памяти в Android?»
Общий принцип один: объект с долгим временем жизни держит ссылку на объект с коротким. В Android «короткий» — это почти всегда Activity, Fragment или View, за которыми тянется вся иерархия и битмапы.
| Причина | Как выглядит | Лечение |
| Context в синглтоне | object Manager { var context: Context } | только applicationContext |
| Нестатический внутренний класс | Handler, Runnable внутри Activity | статический класс + WeakReference, removeCallbacks |
| Неснятая подписка | слушатель, BroadcastReceiver, callback библиотеки | отписка в парном колбэке |
| binding во фрагменте | ссылка на View после onDestroyView | _binding = null |
| корутина вне scope | GlobalScope.launch с ссылкой на UI | viewModelScope, lifecycleScope |
| незавершённая анимация | бесконечный ValueAnimator | остановка в onStop |
// Классика: отложенная задача переживает экран
class MainActivity : AppCompatActivity() {
private val handler = Handler(Looper.getMainLooper())
private val task = Runnable { updateUi() } // держит ссылку на Activity
override fun onStart() {
super.onStart()
handler.postDelayed(task, 60_000)
}
override fun onStop() {
super.onStop()
handler.removeCallbacks(task) // без этой строки — утечка на минуту
}
}
Инструменты: LeakCanary в debug-сборке ловит утечки автоматически и показывает цепочку удержания; Memory Profiler в Android Studio даёт снимок кучи, где после закрытия экрана видно несколько живых экземпляров одной Activity — самый наглядный симптом.
Вопрос 3: «Что такое ANR и как его расследовать?»
ANR — следствие блокировки главного потока. Причины: синхронная сеть или база в UI-потоке, тяжёлые вычисления, взаимная блокировка (deadlock), долгая работа в onCreate приложения, ожидание Binder-вызова к чужому зависшему процессу.
Порядок расследования, который хотят услышать:
- Android vitals в Google Play Console — реальная частота ANR у пользователей и стеки главного потока.
- Трейс ANR из
/data/anr/traces.txt: показывает, на какой строке стоял главный поток. - StrictMode в debug-сборке — ловит дисковые и сетевые операции в главном потоке ещё на этапе разработки.
- Perfetto / System Trace и Macrobenchmark — для jank и медленных кадров.
// включаем StrictMode только в debug: падает при I/O на главном потоке
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}
Рядом с ANR обычно спрашивают про jank: чтобы держать 60 кадров в секунду, на кадр есть примерно 16 миллисекунд (на экране 120 Гц — около 8). Всё, что не уложилось, — пропущенный кадр и заметный рывок при скролле.
Типичные ошибки кандидатов
- Считают, что
notifyDataSetChanged()«просто чуть менее оптимален» — на деле он убивает анимации и перепривязывает всё видимое. - Мутируют список и вызывают
submitListс той же ссылкой — обновления не происходит. - Грузят картинки и форматируют даты в
onBindViewHolder. - Называют утечкой любое высокое потребление памяти. Утечка — это конкретно недостижимый для GC объект, который должен был умереть.
- На вопрос про ANR отвечают «делать всё в фоне», не называя ни одного инструмента диагностики.
Как ответить кратко (20–30 секунд)
«RecyclerView переиспользует ViewHolder через пул, поэтому инфляция происходит только для видимых элементов, а
onBindViewHolderобязан быть дешёвым — он вызывается на каждый кадр скролла. ВместоnotifyDataSetChangedиспользуюListAdapterсDiffUtil: он считает минимальный набор изменений в фоне и даёт корректные анимации; список при этом обязательно передаю новый, иначе разницы не найдётся. Утечки — это когда долгоживущий объект держит короткоживущий: Context в синглтоне, необнулённый binding во фрагменте, неснятые колбэки Handler, корутина вне scope. Ищу их LeakCanary и Memory Profiler. ANR — блокировка главного потока дольше пяти секунд; предотвращаю StrictMode в debug, а расследую по Android vitals и трейсам ANR.»