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

Почему может тормозить список при скроллинге?

2.0 Middle🔥 251 комментариев
#UI и вёрстка#Производительность и оптимизация

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

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

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

# Оптимизация производительности списков в Android

Производительность списков при скроллинге — критически важный аспект пользовательского опыта. Задержки и тормоза обычно возникают из нескольких ключевых областей.

Основные причины тормозов при скроллинге

1. Проблемы с макетом и измерением

Сложные иерархии View — одна из самых частых причин. Каждый дополнительный уровень вложенности увеличивает время на измерение (measure) и размещение (layout):

<!-- Проблемный пример: слишком много вложенности -->
<LinearLayout>
    <RelativeLayout>
        <ConstraintLayout>
            <LinearLayout>
                <!-- Содержимое -->
            </LinearLayout>
        </ConstraintLayout>
    </RelativeLayout>
</LinearLayout>
<!-- Оптимизированный пример: плоская иерархия -->
<ConstraintLayout>
    <!-- Все элементы напрямую внутри одного макета -->
</ConstraintLayout>

Переизмерение на каждый элемент — если размеры элементов динамически меняются в зависимости от контента, система вынуждена постоянно пересчитывать layout.

2. Неэффективная работа RecyclerView.Adapter

Отсутствие DiffUtil приводит к полному обновлению списка вместо точечных изменений:

// Проблемный подход
fun updateData(newItems: List<Item>) {
    items = newItems
    notifyDataSetChanged() // ПЕРЕРИСОВКА ВСЕГО СПИСКА!
}

// Оптимизированный подход с DiffUtil
class ItemDiffCallback : DiffUtil.Callback() {
    override fun getOldListSize() = oldList.size
    override fun getNewListSize() = newList.size
    
    override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
        return oldList[oldPos].id == newList[newPos].id
    }
    
    override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
        return oldList[oldPos] == newList[newPos]
    }
}

3. Дорогие операции в основном потоке

Загрузка изображений в UI-потоке — классическая ошибка:

// НЕПРАВИЛЬНО: загрузка в основном потоке
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val imageUrl = items[position].imageUrl
    Thread { // ОПАСНО: работа с UI из фонового потока!
        val bitmap = loadImageFromNetwork(imageUrl)
        holder.imageView.setImageBitmap(bitmap)
    }.start()
}

// ПРАВИЛЬНО: использование библиотек (Glide, Coil)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    Glide.with(holder.itemView.context)
        .load(items[position].imageUrl)
        .into(holder.imageView)
}

Синхронные операции — чтение из базы данных, парсинг JSON, сложные вычисления прямо в onBindViewHolder.

4. Проблемы с памятью

Утечки памяти в ViewHolder — хранение ссылок на контекст активности:

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    // ПЛОХО: потенциальная утечка памяти
    private val context: Context = itemView.context
    
    // ХОРОШО: использование context из itemView при необходимости
    fun bind(item: Item) {
        itemView.context // используем локально
    }
}

Неэффективное переиспользование ViewHolder — отсутствие разных типов ViewHolder для разных видов элементов.

5. Отсутствие предзагрузки и кэширования

RecyclerView.setItemViewCacheSize() — установка слишком маленького значения (по умолчанию 2):

recyclerView.setItemViewCacheSize(20) // Увеличиваем кэш
recyclerView.setDrawingCacheEnabled(true)

Отсутствие предзагрузки изображений — особенно важно для списков с картинками.

Диагностика проблем

Использование инструментов разработчика

Profile GPU Rendering — показывает, сколько времени тратится на каждый этап рендеринга:

# Включение в настройках разработчика
Settings → Developer options → Profile GPU rendering

Layout Inspector — анализ иерархии View:

// Запуск через Android Studio
Tools → Layout Inspector

Логирование производительности

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
            Log.d("ScrollPerf", "Smooth scrolling started")
        }
    }
})

Оптимизационные техники

1. Оптимизация макетов

  • Использование ConstraintLayout вместо вложенных LinearLayout
  • Применение merge и include для повторяющихся компонентов
  • Установка фиксированных размеров, когда это возможно:
<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:scaleType="centerCrop" />

2. Асинхронная загрузка данных

viewModel.items.observe(viewLifecycleOwner) { items ->
    // Использование viewLifecycleOwner для автоматической отписки
    adapter.submitList(items)
}

// В ViewModel
fun loadItems() {
    viewModelScope.launch {
        val items = repository.loadItems() // Suspend функция
        _items.value = items
    }
}

3. Оптимизация изображений

  • Использование placeholder и error изображений
  • Настройка размеров загружаемых изображений под размер ImageView
  • Кэширование на нескольких уровнях:
Glide.with(context)
    .load(url)
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .override(TARGET_WIDTH, TARGET_HEIGHT) // Оптимизация размера
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .into(imageView)

4. Использование правильных LayoutManager

// LinearLayoutManager с предзагрузкой
val layoutManager = LinearLayoutManager(context)
layoutManager.initialPrefetchItemCount = 10 // Предзагрузка элементов
recyclerView.layoutManager = layoutManager

// Для горизонтальных списков
recyclerView.setHasFixedSize(true) // Если все элементы одного размера

5. Оптимизация onBindViewHolder

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val item = getItem(position)
    
    // Использование тегов для предотвращения повторной привязки
    if (holder.itemView.getTag(R.id.item_id) != item.id) {
        holder.itemView.setTag(R.id.id_item_id, item.id)
        
        // Минимизация вызовов findViewById
        holder.bind(item)
        
        // Асинхронная загрузка тяжелых данных
        loadHeavyDataAsync(item, holder)
    }
}

Проверка результатов

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

  1. Частоту кадров — должно быть стабильно 60 FPS
  2. Потребление памяти — отсутствие утечек через Memory Profiler
  3. Процессорную нагрузку — через CPU Profiler
  4. Время инициализации первого отображения списка

Оптимизация производительности списков — комплексная задача, требующая внимания к деталям на всех уровнях: от архитектуры приложения до конкретной реализации методов адаптера. Систематический подход к диагностике и применению указанных техник позволит достичь плавного скроллинга даже в сложных списках с разнообразным контентом.

Почему может тормозить список при скроллинге? | PrepBro