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 frameRecyclerView.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
| Pattern | Where to apply |
|---|---|
| Pre-allocate in constructor | View.paint, Rect, reusable objects |
| Object pool | Frequently created/destroyed objects in hot paths |
SparseIntArray | Int-keyed maps with primitive values |
StringBuilder reuse | Repeated string formatting |
| Sequence chains | Multi-step collection operations on large sets |
| ViewHolder lambda | Set click listener once in init; swap data in bind |