Урок по треку Android 8 мин

Производительность и утечки памяти за 8 минут

Утечки и LeakCanary, ANR, jank, Baseline Profiles, R8 и профилирование.

Производительность на собесе это три темы: утечки памяти (что держит Activity и как найти), отзывчивость (ANR и jank) и скорость старта (Baseline Profiles, R8). Senior ещё спросят, как это измерять.

Утечка памяти в Android

Утечка это объект, который больше не нужен, но на него есть цепочка сильных ссылок от корня сборки мусора (GC root): статического поля, живого потока, системного сервиса. Сборщик его не освободит. Самый дорогой случай: утечка Activity или Fragment, ведь они держат всю иерархию view, картинки и контекст.

Классические причины:

  1. Статическое поле или синглтон со ссылкой на Activity, View или их Context.
  2. Незарегистрированные листенеры: подписка на системный сервис, LocationManager, шину событий, а отписка забыта.
  3. Долгоживущие асинхронные задачи: поток, Handler.postDelayed, корутина в GlobalScope или колбэк сети, которые захватили Activity и выполняются после её уничтожения.
  4. Внутренние и анонимные классы (inner class, анонимный Runnable, лямбда) хранят ссылку на внешний объект.
  5. ViewModel со ссылкой на View или Activity.
  6. Фрагмент держит 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 и что он может сломать?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.