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

Какую последнюю проблему решал?

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

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

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

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

Анализ и оптимизация производительности списка с сложным контентом в Android приложении

Контекст проблемы

В одном из наших проектов мы столкнулись с серьезной проблемой производительности в ключевом разделе приложения — ленте динамично обновляемого контента. Этот список (RecyclerView) отображал карточки (CardView) с очень насыщенным UI: несколько изображений (анимированные GIF, статичные PNG), текстовые блоки различной стилизации (TextView с кастомными Spannable), индикаторы состояния, интерактивные элементы (кнопки, чекбоксы) и сложную сетку вложенных ViewGroup (ConstraintLayout внутри LinearLayout). При прокрутке списка наблюдались:

  • Значительные фризы и джиттер (падение FPS ниже 30)
  • Пиковый рост потребления памяти при быстрой прокрутке
  • Задержки в отображении новых элементов при подгрузке данных (пагинация)
  • Аномально высокие значения трат CPU в методах onBindViewHolder и onCreateViewHolder

Диагностика и анализ

Первым шагом я использовал Android Profiler (инструменты CPU, Memory и Energy) для выявления узких мест.

Ключевые находки:

  1. Неконтролируемое создание View: В адаптере (Adapter) метод onCreateViewHolder каждый раз создавал новый экземпляр сложного View через LayoutInflater.inflate(), не учитывая типы элементов. Это приводило к постоянному парсингу XML и построению дерева объектов.
  2. Избыточные операции в onBindViewHolder: В методе onBindViewHolder выполнялись тяжелые операции:
    // Проблемный код (упрощенный вариант)
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = dataList[position]
        
        // 1. Загрузка и декодирование GIF "на лету" (каждый раз!)
        holder.gifImageView.setImageResource(loadGifFromNetwork(item.gifUrl))
        
        // 2. Сложная текстовая обработка в каждом элементе
        holder.titleTextView.text = createCustomSpannable(item.title) // Дорогая операция
        
        // 3. Многократные запросы к БД для каждого элемента
        holder.statusIndicator.setStatus(fetchStatusFromDatabase(item.id))
        
        // 4. Установка множества слушателей без проверки
        holder.button.setOnClickListener { /* ... */ }
    }
    
  3. Отсутствие пула ViewHolder: Адаптер не использовал эффективно пул RecyclerView.RecycledViewPool, и типы ViewHolder не были четко разделены для повторного использования.
  4. Проблемы с изображениями: Загрузка GIF и больших PNG без кеширования (LruCache) и оптимизации размеров (Bitmap resize). Не использовались библиотеки типа Glide или Coil.

Решение и оптимизация

Решение было комплексным и включало несколько уровней оптимизации.

1. Рефакторинг адаптера и ViewHolder

  • Введение нескольких типов ViewHolder: Я разделил элементы списка на четкие типы (TYPE_IMAGE_CARD, TYPE_TEXT_CARD, etc.) и настроил пул для их эффективного повторного использования.
    override fun getItemViewType(position: Int): Int {
        return when(dataList[position].contentType) {
            ContentType.IMAGE -> TYPE_IMAGE_CARD
            ContentType.TEXT -> TYPE_TEXT_CARD
            // ...
        }
    }
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when(viewType) {
            TYPE_IMAGE_CARD -> ImageCardViewHolder(inflateView(parent, R.layout.item_image_card))
            TYPE_TEXT_CARD -> TextCardViewHolder(inflateView(parent, R.layout.item_text_card))
            // ...
        }
    }
    
  • Вынос тяжелых операций из onBindViewHolder: Логику создания Spannable, загрузку статусов из БД я перенес в фоновый поток (например, в Coroutine или RxJava), предварительно вычисляя данные и передавая в адаптер уже подготовленные модели.
  • Оптимизация установки слушателей: Слушатели устанавливались только при первом "биндинге", с проверкой, чтобы избежать повторных присваиваний.

2. Оптимизация работы с изображениями

  • Интеграция Glide: Заменил ручную загрузку изображений на Glide с настроенным кешем.
    Glide.with(holder.gifImageView.context)
        .load(item.gifUrl)
        .apply(RequestOptions()
            .diskCacheStrategy(DiskCacheStrategy.ALL)
            .override(TARGET_IMAGE_WIDTH, TARGET_IMAGE_HEIGHT) // Downsampling
        )
        .into(holder.gifImageView)
    
  • Preloading: Для элементов, которые скоро будут отображены (пагинация), реализовал предзагрузку изображений через Glide.preload().

3. Оптимизация layout и отрисовки

  • Упрощение View Hierarchy: Провел анализ с помощью Layout Inspector и упростил вложенность ConstraintLayout, удалив лишние промежуточные ViewGroup.
  • Использование merge и ViewStub: В сложных layout-файлах внедрил <merge> для корневых элементов и <ViewStub> для редко отображаемых блоков.
  • Отключение ненужного hardware acceleration: Для определенных сложных, но статичных элементов отключил setLayerType(View.LAYER_TYPE_HARDWARE, null) чтобы снизить нагрузку на GPU.

4. Профилирование и тонкая настройка

После основных изменений я повторно использовал Profiler и Systrace для проверки:

  • Убедился, что время выполнения onBindViewHolder сократилось с 30-50ms до 5-10ms.
  • Проверил, что пул ViewHolder эффективно работает и количество создаваемых объектов сократилось в 5 раз.
  • С помощью Systrace подтвердил уменьшение времени работы UI Thread (Choreographer frame deadlines стали выполняться стабильно).

Результат

После внедрения всех оптимизаций:

  • FPS стабилизировался на уровне 55-60 даже при быстрой прокрутке.
  • Потребление памяти сократилось на ~40% в этом фрагменте.
  • Время отклика UI на действия пользователя (пагинация, тапы) улучшилось значительно.
  • Общая энергоэффективность повысилась, так как снизилась нагрузка на CPU и GPU.

Эта задача была прекрасным примером того, как системный подход к диагностике (Profiler), глубокое понимание внутренних механизмов Android (RecyclerView, ViewHolder, рендеринг) и применение современных библиотек и практик позволяют решить сложную проблему производительности, напрямую влияющую на пользовательский опыт.