Производительность на собесе это три темы: утечки памяти (что держит Activity и как найти), отзывчивость (ANR и jank) и скорость старта (Baseline Profiles, R8). Senior ещё спросят, как это измерять.
Утечка памяти в Android
Утечка это объект, который больше не нужен, но на него есть цепочка сильных ссылок от корня сборки мусора (GC root): статического поля, живого потока, системного сервиса. Сборщик его не освободит. Самый дорогой случай: утечка Activity или Fragment, ведь они держат всю иерархию view, картинки и контекст.
Классические причины:
- Статическое поле или синглтон со ссылкой на Activity, View или их Context.
- Незарегистрированные листенеры: подписка на системный сервис,
LocationManager, шину событий, а отписка забыта. - Долгоживущие асинхронные задачи: поток,
Handler.postDelayed, корутина вGlobalScopeили колбэк сети, которые захватили Activity и выполняются после её уничтожения. - Внутренние и анонимные классы (
inner class, анонимныйRunnable, лямбда) хранят ссылку на внешний объект. - ViewModel со ссылкой на View или Activity.
- Фрагмент держит binding после
onDestroyView.
class ProfileFragment : Fragment(R.layout.fragment_profile) {
private var _binding: FragmentProfileBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentProfileBinding.bind(view)
}
override fun onDestroyView() {
_binding = null // иначе фрагмент в back stack держит всю view
super.onDestroyView()
}
}
Лечение: использовать applicationContext в долгоживущих объектах, отписываться симметрично (onStart/onStop, DisposableEffect), корутины в viewModelScope/lifecycleScope, lifecycle-aware компоненты. WeakReference помогает в редких случаях, но чаще маскирует проблему.
Как искать утечки
- LeakCanary: подключается в debug, после уничтожения Activity/Fragment/ViewModel проверяет, собрался ли объект, и если нет, снимает дамп кучи и показывает цепочку ссылок до GC root.
- Android Studio Memory Profiler: дамп кучи, поиск Activity, которые уже уничтожены.
- Признаки в проде: рост памяти,
OutOfMemoryErrorв отчётах падений, частые паузы GC.
Отзывчивость: ANR и jank
ANR появляется, если главный поток не отвечает на ввод 5 секунд. Причины: ввод-вывод на главном потоке, блокировки (synchronized, ожидание другого потока), долгие вычисления, тяжёлый BroadcastReceiver. Отчёты ANR видны в Android Vitals Play Console, содержат стек главного потока.
Jank это пропущенные кадры. Бюджет кадра 16 мс при 60 Гц. Источники: тяжёлая работа в onBindViewHolder или в теле composable, лишние рекомпозиции, глубокая вложенность разметки, декодирование картинок на главном потоке, частые аллокации и GC.
Инструменты: Perfetto и System Trace, Android Studio Profiler (CPU), JankStats, Macrobenchmark для автоматических замеров, Layout Inspector для рекомпозиций.
Старт приложения
Холодный старт: процесс создаётся с нуля, затем Application.onCreate, создание Activity, первый кадр. Метрики: Time To Initial Display (TTID) и Time To Full Display (TTFD, отмечается reportFullyDrawn()).
Как ускорить:
- ничего тяжёлого в
Application.onCreate: ленивая инициализация SDK, App Startup library, перенос в фон; - Baseline Profiles: список классов и методов критичных путей, которые ART компилирует заранее при установке, а не интерпретирует при первом запуске. Особенно важно для Compose, который поставляется как библиотека, а не часть системы. Генерируются Macrobenchmark-тестом, дают заметный выигрыш старта и плавности;
- Startup Profiles для оптимизации раскладки dex;
- Splash Screen API вместо отдельной Activity-заставки.
R8
R8 в release-сборке: сжатие (удаление неиспользуемого кода), обфускация (короткие имена), оптимизация (инлайнинг, удаление мёртвых веток). Уменьшает APK и ускоряет код. Ломает то, что вызывается через рефлексию: нужны правила -keep (большинство библиотек приносят свои правила сами). Для расшифровки стектрейсов из прода нужен файл mapping.txt, Play Console и Crashlytics принимают его автоматически.
Память и картинки
Картинки самый большой потребитель памяти. Bitmap 1080×1920 в ARGB_8888 занимает около 8 МБ. Библиотеки Coil и Glide декодируют под размер view, кешируют в памяти и на диске и отменяют загрузку для переиспользованных элементов списка.
onTrimMemory сигнализирует, что системе не хватает памяти: можно очистить кеши.
Частые ошибки
- Контекст Activity в синглтоне.
- Отсутствие отписки от листенеров и
GlobalScope. - Binding фрагмента без обнуления.
- Тяжёлый
Application.onCreate. - Замеры производительности в debug-сборке (особенно Compose): она в разы медленнее release.
- R8 без проверки release-сборки и падения из-за рефлексии.
Что спросят на собесе
- Что такое утечка памяти в Android и какие у неё типичные причины?
- Как работает LeakCanary?
- Чем ANR отличается от jank? Как найти причину каждого?
- Как ускорить холодный старт? Что такое Baseline Profiles?
- Что делает R8 и что он может сломать?