androidengineers.Book a session

Memory Management

Object Pooling & Allocations

article20 minHard

Every object allocation in Android eventually triggers a GC cycle. GC pauses — even the concurrent ones in ART — cause frame drops. Reducing allocations in hot paths (RecyclerView's onDraw, animation callbacks, high-frequency sensors) directly reduces jank.

Where Allocations Hurt Most

Allocations are cheap for one-time setup code. They're expensive in:

  • View.onDraw() — called every frame
  • RecyclerView.Adapter.onBindViewHolder() — called for each visible item
  • Animation callbacks (Animator.AnimatorUpdateListener)
  • High-frequency event handlers (touch, location, sensors)

Detect allocations: Android Studio Profiler → Memory tab → Record Allocation. Sort by allocation count to find hot spots.

Pattern 1: Reuse Objects Instead of Allocating

// ❌ Allocates a new Paint every frame — extremely wasteful
class MyView(context: Context) : View(context) {
    override fun onDraw(canvas: Canvas) {
        val paint = Paint()  // ← allocation on every draw frame
        paint.color = Color.RED
        canvas.drawCircle(100f, 100f, 50f, paint)
    }
}

// ✅ Pre-allocate; onDraw just uses the existing object
class MyView(context: Context) : View(context) {
    private val paint = Paint().apply {
        color = Color.RED
        isAntiAlias = true
    }

    override fun onDraw(canvas: Canvas) {
        canvas.drawCircle(100f, 100f, 50f, paint)  // no allocation
    }
}

Pattern 2: Object Pool

A pool of pre-allocated, reusable instances:

class ObjectPool<T>(
    private val maxSize: Int,
    private val factory: () -> T,
    private val reset: (T) -> Unit = {}
) {
    private val pool = ArrayDeque<T>(maxSize)

    fun acquire(): T = if (pool.isEmpty()) factory() else pool.removeLast()

    fun release(obj: T) {
        if (pool.size < maxSize) {
            reset(obj)
            pool.addLast(obj)
        }
    }
}

// Example: pool of RectF objects for drawing
val rectPool = ObjectPool(
    maxSize = 20,
    factory = { RectF() },
    reset = { rect -> rect.setEmpty() }
)

// In onDraw:
val rect = rectPool.acquire()
rect.set(x, y, x + width, y + height)
canvas.drawRect(rect, paint)
rectPool.release(rect)

Pattern 3: Avoid Autoboxing in Hot Paths

Kotlin's List<Int> boxes each Int into an Integer object. Use primitive arrays or SparseArray/SparseIntArray in hot paths:

// ❌ Boxes Int → Integer for each element
val counts = HashMap<Int, Int>()
counts[id] = (counts[id] ?: 0) + 1

// ✅ Uses primitive int arrays internally
val counts = SparseIntArray()
counts.put(id, counts.get(id, 0) + 1)

// ✅ Kotlin's IntArray for sequences
val values = IntArray(100)
values[0] = 42

Pattern 4: StringBuilder Reuse

// In a class that formats strings frequently:
class LogFormatter {
    private val sb = StringBuilder(256)

    fun format(level: String, tag: String, message: String): String {
        sb.setLength(0)  // reset without allocation
        sb.append('[').append(level).append(']')
            .append(' ').append(tag).append(": ")
            .append(message)
        return sb.toString()
    }
}

Pattern 5: RecyclerView ViewHolder Reuse

ViewHolder is already the pooling mechanism for RecyclerView. But bindings inside onBindViewHolder can still allocate:

// ❌ Creates new OnClickListener on every bind
class ArticleViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    fun bind(article: Article) {
        itemView.setOnClickListener {
            openArticle(article)  // lambda allocation per bind
        }
    }
}

// ✅ Set listener once in constructor; update data separately
class ArticleViewHolder(
    view: View,
    private val onArticleClick: (Article) -> Unit
) : RecyclerView.ViewHolder(view) {
    private var currentArticle: Article? = null

    init {
        itemView.setOnClickListener { currentArticle?.let(onArticleClick) }
    }

    fun bind(article: Article) {
        currentArticle = article  // no new listener allocation
        // bind fields...
    }
}

Kotlin-Specific Optimizations

// Prefer sequences over chains of collection operations (avoids intermediate lists)
val result = items
    .asSequence()          // lazy — no intermediate list
    .filter { it.active }
    .map { it.value }
    .take(10)
    .toList()              // only materializes here

// Use inline functions (no lambda object created at call site)
inline fun measureTime(block: () -> Unit): Long {
    val start = System.nanoTime()
    block()
    return System.nanoTime() - start
}

Key Takeaways

PatternWhere to apply
Pre-allocate in constructorView.paint, Rect, reusable objects
Object poolFrequently created/destroyed objects in hot paths
SparseIntArrayInt-keyed maps with primitive values
StringBuilder reuseRepeated string formatting
Sequence chainsMulti-step collection operations on large sets
ViewHolder lambdaSet click listener once in init; swap data in bind

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Object Pooling & Allocations | Android System Design | Android Engineers