← Назад к вопросам

Какой баг возник по твоей вине?

1.2 Junior🔥 142 комментариев
#Опыт и софт-скиллы

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Самый значимый баг по моей вине

В моей практике самым серьезным и сложным багом, возникшим по моей вине, был проблема с утечкой жизненного цикла (Lifecycle Leak) в сложном приложении для управления медиа-контентом. Баг проявлялся в форме случайного падения (crash) приложения на устройствах с небольшим объемом памяти после длительного использования, особенно при частом переключении между фрагментами (Fragments) в навигационном стеке.

Контекст и причина возникновения

Я разрабатывал модуль, который должен был асинхронно загружать и декодировать большие изображения из сети в Background Thread, а затем отображать их в RecyclerView внутри Fragment. Для оптимизации и избегания повторных загрузок я создал синглтон-класс ImageCacheManager, который хранил декодированные Bitmap в LruCache и предоставлял их по запросу.

Ключевая ошибка заключалась в регистрации слушателей (Listeners / Callbacks):

class ImageCacheManager {
    private val cache = LruCache<String, Bitmap>(MAX_CACHE_SIZE)
    private val listeners = mutableSetOf<ImageLoadListener>()

    fun loadImage(url: String, listener: ImageLoadListener) {
        // Если изображение в кэше — сразу возвращаем
        val cached = cache.get(url)
        if (cached != null) {
            listener.onImageLoaded(cached)
            return
        }
        // Если нет — начинаем загрузку и добавляем listener в набор
        listeners.add(listener)
        startNetworkLoad(url)
    }

    private fun startNetworkLoad(url: String) {
        // Асинхронная загрузка...
        // После завершения:
        onLoadCompleted(url, bitmap) {
            cache.put(url, bitmap)
            listeners.forEach { it.onImageLoaded(bitmap) }
            listeners.clear() // ОЧИСТКА! Но это не всегда происходило в моем первоначальном коде.
        }
    }
}

interface ImageLoadListener {
    fun onImageLoaded(bitmap: Bitmap)
}

Мой Fragment регистрировался как ImageLoadListener:

class MediaFragment : Fragment(), ImageLoadListener {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        ImageCacheManager.getInstance().loadImage(imageUrl, this)
    }

    override fun onImageLoaded(bitmap: Bitmap) {
        // Устанавливаем изображение в ImageView
    }
}

Где была моя вина:

  1. Я не учел жизненный цикл Fragment. Если Fragment был уничтожен (например, пользователь перешел назад в BackStack, или система уничтожила Activity), но асинхронная загрузка еще не завершилась, мой ImageCacheManager продолжал хранить ссылку (reference) на этот уничтоженный Fragment в коллекции listeners.
  2. Ошибка в логике очистки. В первоначальной реализации метод listeners.clear() вызывался только при успешной загрузке. Если же сетевой запрос завершался с ошибкой или был прерван, коллекция не очищалась, и ссылки на Fragment оставались.
  3. Игнорирование возможности повторной регистрации. Разные Fragment могли запросить одно и то же изображение. Моя логика добавляла каждого нового слушателя в набор, даже если загрузка уже была в процессе, что увеличивало количество потенциальных утечек.

Последствия и диагностика

Баг проявлялся не сразу, что затрудняло диагностику. Приложение работало нормально несколько часов или дней. Но на устройствах с 4GB RAM или меньше начинались странные crash с сообщениями в логах, связанными с OutOfMemoryError или попытками обращения к разрушенным View. После анализа с помощью Android Studio Profiler и LeakCanary была обнаружена цепочка утечек:

  • ImageCacheManager → множество ImageLoadListener → уничтоженные экземпляры MediaFragment → их View и контексты (Context).

Это приводило к:

  • Утечке памяти (Memory Leak) — невозможности освободить ресурсы, связанные с уничтоженными фрагментами.
  • Неопределенному поведению (undefined behavior) — попыткам вызвать onImageLoaded() на объекте, который уже не существует, что иногда вызывало crash.

Решение и исправление

Решение потребовало пересмотра архитектуры взаимодействия с жизненным циклом:

  1. Замена сильной ссылки на слабую (WeakReference) для слушателей.
    private val listeners = mutableSetOf<WeakReference<ImageLoadListener>>()
    
    Но это лишь частичное решение, так как нужно было еще фильтровать `null` ссылки.

  1. Интеграция с жизненным циклом Lifecycle — лучший подход. Я переделал API ImageCacheManager, чтобы он принимал LifecycleOwner и автоматически отменял/очищал callback при уничтожении.

    fun loadImage(url: String, lifecycleOwner: LifecycleOwner, listener: ImageLoadListener) {
        val job = startNetworkLoad(url) // Возвращает Job/Cancellable
        lifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { source, event ->
            if (event == Lifecycle.Event.ON_DESTROY) {
                job.cancel()
                // Удаляем listener из любого внутреннего списка
            }
        })
        // ... остальная логика
    }
    
  2. Использование готовых паттернов и библиотек. В итоге я отказался от собственной реализации и интегрировал Glide для загрузки изображений, которая уже имеет глубокую интеграцию с жизненным циклом Activity/Fragment и автоматически управляет запросами.

Выводы и уроки

Этот баг стал для меня мощным уроком:

  • Важность понимания жизненного цикла (Lifecycle Awareness) в Android является первостепенной. Любая долговременная операция или callback должен быть явно привязан к жизненному циклу компонента.
  • Синглтоны и глобальные менеджеры — потенциальные источники утечек, если они хранит ссылки на контексты или UI-компоненты.
  • Тестирование на длительных сценариях — необходимо имитировать не просто открытие/закрытие, а сложные переходы в BackStack, повороты экрана и восстановление после уничтожения (process death).
  • Инструменты профилирования (Profiler, LeakCanary) должны использоваться постоянно во время разработки, а не только при возникновении проблем.

Этот опыт жестко, но эффективно научил меня принципам чистой архитектуры (Clean Architecture) и ответственного управления ресурсами (Resource Management) в мобильной разработке.