androidengineers.Book a session

Memory Management

Bitmap Strategies & Large Objects

article25 minMedium

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-patternFix
Bitmap.createScaledBitmap on full-size imageUse inSampleSize before decoding
Storing Bitmap in static fieldStore URL/path; decode on demand
Bitmap in onDrawCache 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 thumbnailUse 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)
    }
}

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Bitmap Strategies & Large Objects | Android System Design | Android Engineers