Почему частое очищение объектов с Garbage Collector в методе onDraw может привести к тормозящему UI

Ответ

Частые вызовы GC в onDraw() приводят к пропускам кадров (jank), т.к. сборка мусора требует приостановки UI-потока.

Причины:

  1. Создание временных объектов (например, new Paint(), new Rect()) в цикле onDraw()
  2. Автоупаковка примитивов (например, Integer.valueOf())

Решение:

  • Выносите создание объектов в инициализацию (поля класса)
  • Используйте canvas.save()/restore() вместо создания новых матриц
  • Для сложных вычислений - кешируйте результаты
// Плохо:
override fun onDraw(canvas: Canvas) {
    val paint = Paint() // Создается каждый кадр
    canvas.drawText("Hello", 0f, 0f, paint)
}

// Хорошо:
private val paint = Paint() // Инициализируется один раз

override fun onDraw(canvas: Canvas) {
    canvas.drawText("Hello", 0f, 0f, paint)
}

Ответ 18+ 🔞

А, слушай, вот тебе история про то, как твой красивый интерфейс превращается в слайд-шоу из-за одной тупой, но вечной ошибки. Ёпта, это же классика, как будто на дворе 2002-й год и мы только учимся не создавать объекты в цикле.

Короче, суть в чём. Твой метод onDraw() вызывается дохуя раз в секунду, чтобы всё плавно рисовалось. И если ты внутри него начинаешь плодить временные объекты — новые Paint, Rect, Path — то ты, сам того не желая, устраиваешь мусорную вечеринку. А потом приходит уборщик — Garbage Collector (GC), — и ему, этому пидарасу шерстяному, нужно всё за собой прибрать. А чтобы подмести, он вынужден остановить весь UI-поток. Представь: ты смотришь кино, а его каждые пять секунд ставят на паузу, чтобы дворник прошёлся с метлой. Вот это и есть твой «jank» — эти дерганые, пропущенные кадры. Волнение ебать, когда смотришь на это.

Основные причины, из-за которых это происходит — просто пиздопроебибна своей очевидностью:

  1. Создание объектов прямо в onDraw(). Самый частый грех — val paint = Paint() внутри метода. Это же, блядь, создаётся каждый раз, когда нужно что-то нарисовать! Через пару секунд у тебя овердохуища мусора, и GC начинает свою пляску.
  2. Автоупаковка (autoboxing). Когда ты, например, в список List<Int> пихаешь обычный int, а система тихонечко создаёт для тебя объект Integer. Делает это она через Integer.valueOf(). В цикле onDraw() это тоже смерть.

Что делать, чтобы не было хиросимы?

  • Выноси всё, что можно, в поля класса. Кисти (Paint), прямоугольники (Rect), пути (Path) — инициализируй их один раз, при создании вьюхи, а в onDraw() только используй. Это же логично, ёклмн!
  • Вместо того чтобы создавать новые трансформации (Matrix), используй canvas.save() и canvas.restore(). Экономия — мать порядка.
  • Сложные вычисления, которые не меняются каждый кадр, — кешируй. Не вычисляй одно и то же по сто раз, будь умнее.

Смотри, вот наглядный пример, чтобы в сердцу мать дошло:

// Плохо: делаем так — и получаем слайд-шоу. Каждый кадр новая кисть. Манда с ушами.
override fun onDraw(canvas: Canvas) {
    val paint = Paint() // Создается КАЖДЫЙ КАДР! Пиздец.
    canvas.drawText("Hello", 0f, 0f, paint)
}

// Хорошо: делаем так — и живём спокойно. Кисть одна на всю жизнь вьюхи.
private val paint = Paint() // Инициализируется ОДИН РАЗ, когда создаётся вьюха

override fun onDraw(canvas: Canvas) {
    canvas.drawText("Hello", 0f, 0f, paint) // Просто рисуем готовым инструментом
}

Вот и вся магия. Не устраивай мусорный апокалипсис в методе, который должен летать, и всё будет охуенно. А то будешь потом сидеть и думать: «Э, бошка, думай, почему же у меня всё тормозит?». А причина-то — хуй в пальто, который ты сам себе и создал.