The Error
You're loading an image โ maybe from a gallery picker, a camera capture, or a remote URL โ and the app crashes with:
java.lang.OutOfMemoryError: Failed to allocate a 52428800 byte allocation with 8388608 free bytes and 7MB until OOM
That 52428800 bytes is 50 MB. Android tried to allocate 50 MB for a single bitmap, found only ~8 MB free in the heap, and bailed. The app is dead.
Why This Happens
Android decodes images into raw pixel data in memory. A 12 MP photo (4032ร3024) decoded as ARGB_8888 uses:
4032 ร 3024 ร 4 bytes = ~46.5 MB
That's one photo. The default heap on many mid-range devices is 48โ128 MB total, shared with everything else the app is doing. Load two or three uncompressed bitmaps and you've blown the budget.
Common triggers:
- Loading full-resolution camera photos directly into an
ImageView - Decoding images inside a
RecyclerViewwithout recycling bitmaps - Using
BitmapFactory.decodeFile()without sampling options - Keeping references to bitmaps in static fields or singletons
- Loading images with Glide/Picasso but bypassing their caching (e.g., calling
.skipMemoryCache(true)everywhere)
Step 1 โ Confirm the Source in Logcat
Find the exact call that's crashing before touching any code. Run the app, reproduce the crash, then filter Logcat:
adb logcat | grep -E "OutOfMemoryError|BitmapFactory|alloc"
Or in Android Studio, filter by OutOfMemoryError. The stack trace will point to the exact line โ usually a BitmapFactory.decode* call or an image library loader.
Step 2 โ If You're Using Raw BitmapFactory
Never decode a full image without sampling it down to fit the target view. Use BitmapFactory.Options:
fun decodeSampledBitmap(filePath: String, reqWidth: Int, reqHeight: Int): Bitmap {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true // decode size only, no pixels
}
BitmapFactory.decodeFile(filePath, options)
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false
return BitmapFactory.decodeFile(filePath, options)
}
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
val (height, width) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
Pass the actual display size of your ImageView โ not a hardcoded guess:
imageView.post {
val bmp = decodeSampledBitmap(filePath, imageView.width, imageView.height)
imageView.setImageBitmap(bmp)
}
This can shrink a 46 MB allocation down to 3โ4 MB with zero visible quality loss in a thumbnail.
Step 3 โ Switch to RGB_565 for Non-Transparent Images
Photos and background images rarely need alpha transparency. Use RGB_565 instead of the default ARGB_8888 โ it stores 2 bytes per pixel instead of 4, cutting memory in half:
val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.RGB_565
inSampleSize = 4 // or your calculated value
}
val bitmap = BitmapFactory.decodeFile(filePath, options)
Step 4 โ If You're Using Glide
Glide handles most of this automatically. OOM still sneaks in when you override the target size to full resolution, or load into a view that's wrap_content with no explicit dimensions.
Explicitly cap the dimensions and use the lighter pixel format:
Glide.with(context)
.load(imageUrl)
.override(800, 600) // cap dimensions
.format(DecodeFormat.PREFER_RGB_565) // half the memory
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(imageView)
For full-screen banners where you want screen-width resolution without a fixed pixel size:
Glide.with(context)
.load(imageUrl)
// .override(Target.SIZE_ORIGINAL) // avoid this unless you truly need full res
.override(Resources.getSystem().displayMetrics.widthPixels, 0) // screen width, auto height
.into(imageView)
Also check that you're not holding a Target reference in a static field โ that's a reliable way to prevent Glide from ever releasing the bitmap.
Step 5 โ If You're Using Picasso
Picasso's API is more explicit about sizing. Set resize() and onlyScaleDown() together so you cap large images without accidentally upscaling small ones:
Picasso.get()
.load(imageUrl)
.resize(800, 600)
.onlyScaleDown()
.centerCrop()
.into(imageView)
For local files, pass the URI rather than the raw file path. Picasso handles file:// URIs cleanly and won't double-buffer the data the way a raw path sometimes does.
Step 6 โ Recycle Bitmaps You No Longer Need
Managing bitmaps manually โ without Glide or Picasso โ means you own the cleanup. Call recycle() when the view detaches:
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
val drawable = imageView.drawable
if (drawable is BitmapDrawable) {
drawable.bitmap?.recycle()
}
imageView.setImageDrawable(null)
}
One hard rule: never call recycle() on a bitmap that's still being displayed. It causes Canvas: trying to use a recycled bitmap crashes that are worse to debug than the OOM you started with.
Step 7 โ Check for Memory Leaks with the Profiler
OOM from leaks looks different from a single large allocation. Heap usage climbs steadily on each navigation and never drops back. Use Android Studio's Memory Profiler to spot this:
- Run the app in debug mode โ View โ Tool Windows โ Profiler
- Click the Memory track
- Navigate to the screen that loads images, go back, repeat 5 times
- Click Force GC (the garbage can icon)
- If heap usage doesn't drop back toward baseline, you have a leak
Click Capture heap dump and look for Bitmap instances that should have been released. Even one stuck bitmap can be 46 MB of permanent overhead.
Step 8 โ Increase largeHeap Only as a Last Resort
Adding android:largeHeap="true" to your AndroidManifest.xml roughly doubles the available heap on most devices:
<application
android:largeHeap="true"
...>
It buys time, nothing more. The crash will return as soon as the app loads slightly more data, and some OEM devices ignore the flag entirely. Use it only while you're working on the real fix โ or for apps with genuinely heavy memory needs like photo editors.
Verify the Fix
- Reproduce the exact flow that crashed before โ no crash means the fix is holding
- Watch the Memory Profiler during image loading and confirm allocations stay flat
- Check Logcat for any remaining
Bitmap allocationwarnings - Test on a low-end device (1โ2 GB RAM), or set an emulator to 512 MB RAM โ problems that hide on a flagship show up immediately here
Lessons Learned
- Never call
BitmapFactory.decode*withoutinJustDecodeBounds+inSampleSize - Glide and Picasso handle the hard parts โ let them manage the lifecycle rather than reimplementing it manually
- A 12 MP photo is always ~46 MB in memory, no matter how small the JPEG is on disk
RGB_565is free memory savings for any image without transparency- Leaks are harder to spot than single large allocations โ profile first before assuming it's a one-time load problem

