Bitmaps are the single largest memory consumer in most Android apps. A 12 MP camera photo, decoded naively to ARGB_8888, consumes ~48 MB — more than many app's entire heap budget. Handling them correctly is non-negotiable.
Bitmap Memory Cost
Width × Height × Bytes per pixel = Memory
4000 × 3000 × 4 (ARGB_8888) = 48 MB
4000 × 3000 × 2 (RGB_565) = 24 MB
4000 × 3000 × 1 (ALPHA_8) = 12 MB
Strategy 1: Decode at Display Size (inSampleSize)
Never load a full-resolution bitmap into memory for a 100dp thumbnail. Calculate a subsample factor:
fun decodeSampledBitmapFromResource(
resources: Resources,
resId: Int,
reqWidth: Int,
reqHeight: Int
): Bitmap {
// First: decode bounds only (no memory allocation)
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeResource(resources, resId, options)
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false
return BitmapFactory.decodeResource(resources, resId, options)
}
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
val (height, width) = options.run { outHeight to outWidth }
var inSampleSize = 1
while (height / inSampleSize / 2 >= reqHeight && width / inSampleSize / 2 >= reqWidth) {
inSampleSize *= 2
}
return inSampleSize
}
Strategy 2: Use a Pixel Format Appropriate for the Content
// ARGB_8888: full quality, 4 bytes/pixel — for photos, detailed images
val options = BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.ARGB_8888 }
// RGB_565: 2 bytes/pixel, no alpha — for opaque images like wallpapers
val options = BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.RGB_565 }
// HARDWARE: bitmap stored in GPU memory — fastest for display, can't be read/modified
val options = BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.HARDWARE }
Strategy 3: Bitmap Pooling (BitmapPool)
Avoid allocating new Bitmaps — reuse existing ones of the same size:
// BitmapPool from Glide — available if you have Glide in your project
val pool = LruBitmapPool(10 * 1024 * 1024L) // 10 MB pool
// Reuse an existing bitmap when decoding
val reusableBitmap = pool.get(width, height, Bitmap.Config.ARGB_8888)
val options = BitmapFactory.Options().apply {
inBitmap = reusableBitmap // reuse this allocation
inMutable = true
}
val bitmap = BitmapFactory.decodeFile(path, options)
pool.put(bitmap) // return when done
inBitmap reuse requires the reused bitmap to be mutable and at least as large as the new bitmap. Android 4.4+ allows reuse of any same-sized bitmap regardless of dimensions.
Strategy 4: Let Libraries Handle It
Glide and Coil implement all of the above automatically:
// Glide: auto-samples, caches, pools, cancels
Glide.with(context)
.load(url)
.override(100, 100) // decode at display size
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.into(imageView)
// Coil: same semantics
imageView.load(url) {
size(100, 100)
placeholder(R.drawable.placeholder)
error(R.drawable.error)
memoryCachePolicy(CachePolicy.ENABLED)
}
Large Object Anti-Patterns
| Anti-pattern | Fix |
|---|---|
Bitmap.createScaledBitmap on full-size image | Use inSampleSize before decoding |
| Storing Bitmap in static field | Store URL/path; decode on demand |
Bitmap in onDraw | Cache decoded Bitmap; never decode inside onDraw |
Forgetting bitmap.recycle() (pre-API 26) | Use Glide/Coil pool; or call recycle() explicitly when done |
| Loading original resolution in thumbnail | Use Glide override(width, height) |
Other Large Object Concerns
// Large string allocations — use StringBuilder
val sb = StringBuilder(capacity = 1024)
for (item in items) sb.append(item)
val result = sb.toString()
// Large byte arrays from I/O — stream instead of loading all at once
val buffer = ByteArray(8192) // 8 KB buffer
inputStream.use { stream ->
while (stream.read(buffer) != -1) {
processChunk(buffer)
}
}