Android Memory Optimization Series:
- Android Code Memory Optimization Suggestions - Android (Official)
- Android Code Memory Optimization Suggestions - Java (Official)
- Android Code Memory Optimization Suggestions - Android Resources
- Android Code Memory Optimization Suggestions - OnTrimMemory
I wrote this because understanding common memory problems before writing the code can save a lot of diagnosis later. We will look at Bitmaps, database queries, list reuse, and drawing: who creates each resource, what keeps it alive, and when it is no longer needed. Retention, repeated allocation, and leaks need different remedies.
Technical review: October 13, 2026. First published in 2015. Recycling every Bitmap manually is no longer sound general advice, and pixel visibility in MAT depends on the Android version. Cursor, Adapter, and Handler examples are completed below, with corrections to NinePatch, lifecycle, and ownership guidance. The eight sections and two illustrations are retained.
Android Resource Optimization
1. Bitmap Optimization
Bitmaps can dominate memory in image-heavy apps, but not every Android memory problem comes from them. Measure live objects, caches, and allocation sources before choosing an optimization.
Pixel storage has changed across versions, so not every HPROF contains a corresponding byte[]:
| Android version | Typical pixel storage |
|---|---|
| Android 2.3.x and earlier (API 10 and below) | Native memory; the Java Bitmap object is a wrapper. |
| 3.0 / API 11 through 7.1 / API 25 | Managed heap alongside the Bitmap. |
| 8.0 / API 26 and later | Native heap; the Java wrapper alone does not represent all pixel memory. |
See Managing Bitmap Memory. Interpret the actual bitmap type, graphics allocations, and device implementation using the available tools. An uncollected object is not automatically a leak: bounded caches and images being displayed or processed have legitimate owners.
1.1 Manually Releasing Bitmap Resources
Do not treat recycle() as mandatory cleanup for every image. The old guide specifically recommended explicit recycling for API 10 and earlier. Current code should first address decode sizes, cache limits, and ownership.
The current Bitmap.recycle() contract describes immediate pixel-memory release for images certainly no longer needed, such as an exclusively owned intermediate transformation buffer. It also warns against recycling Bitmaps used by Views or potentially referenced elsewhere. GC still manages the Java wrapper. The old mBuffer and nativeRecycle() excerpt describes an older implementation, not every version.
isRecycled() == false only says the bitmap has not been marked recycled. It does not establish exclusive ownership or remove races. Dropping your reference, detaching an image, and cancelling a load are different operations; assigning null to a method parameter does not clear the caller’s field. Follow an image library’s target-cleanup and bitmap-pool ownership rules rather than recycling its images independently.
1.2 Releasing ImageView Images
getDrawable() can return a VectorDrawable, ColorDrawable, LayerDrawable, or another type, so a direct BitmapDrawable cast is unsafe. Even a BitmapDrawable can contain a bitmap shared with caches or other Views.
After cancelling relevant asynchronous loading, this helper removes the ImageView’s image reference on the UI thread:
1 | private static void clearImageViewImage(android.widget.ImageView imageView) { |
It detaches this View’s image without promising immediate release of all pixel memory. Stop asynchronous requests from delivering into the old target too, or the image can be repopulated after clearing. Prefer the library’s clear/cancel interface for library-managed targets.
1.3 Releasing ImageView Backgrounds
The background and src hold separate references. A background can be any Drawable. This helper performs no cast and does not recycle a shared bitmap; setBackground() is available from API 16:
1 | private static void clearViewBackground(android.view.View view) { |
Clear only backgrounds that are no longer needed, preserving visual and state feedback. Other Views, caches, and drawing work may still retain the bitmap; one null assignment does not establish that all use has ended.
1.4 Use Nine-Patch for Suitable Stretchable Backgrounds
NinePatch is itself a PNG format. Its app-resource extension is .9.png, not .9.webp. Border markers define stretchable and content regions. It suits backgrounds such as bubbles and borders, not every photograph, and choosing the wrong stretch region can still distort an image.
A small source image can replace several larger backgrounds, but cost still depends on its dimensions, decoding, and use. Android Studio’s Draw 9-patch tool edits these markers and previews different sizes.

1.5 Decode Large Images for the Target Size
Reducing encoded file size and decoding fewer pixels are different optimizations. Lowering JPEG quality generally makes the compressed file smaller; with unchanged dimensions and pixel format, decoded pixel memory may remain similar. Decoding the full image and then scaling it has already incurred the larger peak allocation.
For thumbnails, determine the required size before decoding. Account for zooming or editing if needed. With BitmapFactory:
- Set
inJustDecodeBounds = trueto read dimensions and type without allocating the full pixels. Check thatoutWidthandoutHeightare valid before using them. - Compute
inSampleSizefor the target pixel dimensions, then disable bounds-only decoding and load the sampled image. Resource-density scaling can also affect output size, so inspect the actual result.
From Android 9 (API 28), ImageDecoder.setTargetSize() can specify output dimensions; preserve the aspect ratio required by the task. When using an existing loading library, pass the right target size and let it coordinate decoding, caching, and cancellation.
Avoid blocking the UI thread with decoding and large-image processing. See Loading Large Bitmaps Efficiently.
2. Unclosed Database Cursors
Code obtaining a Cursor directly is responsible for closing it on success and failure paths. Do not close only when moveToNext() succeeds, or treat a null query result as an empty result set.
This complete example uses Java 7+ try-with-resources on API 19 and later. Counting demonstrates the resource scope. Supply a URI the caller is authorized to access and the necessary projection, and run the query on an appropriate background thread:
1 | import android.content.ContentResolver; |
Zero means that a valid Cursor contains no records; this example treats a null Cursor as an error. It does not copy all rows into a collection, but getCount() can still perform real query work and should not be assumed cheap. Explicit finally cleanup in older projects must also cover failures; do not silently swallow every close exception.
When ownership has been transferred to a Loader, adapter, or other manager, follow that component’s contract instead of closing its Cursor prematurely. See Cursor.close() and ContentResolver.query().
3. Not Recycling convertView in Adapters
ListView’s convertView is an old row available for reuse. It can be null initially or when no suitable row is available. Recreating a row every time adds allocation, layout, and GC pressure, but does not automatically cause a growing leak.
Check the View type and bind current data on both the creation and reuse paths.

This complete single-row-type adapter uses a standard layout. It copies the input list at construction and only demonstrates display, without a data-update API:
1 | import android.content.Context; |
Complex rows must also reset visibility, selection, and old image requests so state from the previous item does not leak into the next one. Implement getItemViewType() and getViewTypeCount() correctly for multiple layouts. RecyclerView also requires complete binding and appropriate recycling; it cannot remove every application-owned reference automatically. See Adapter.getView().
4. Releasing Object References
To understand retention, follow strong references back to their owners and GC roots. Setting one variable to null removes only that reference; another cache, callback, or queue can still retain the object.
Scenario A: Handlers and Runnables
The original example copied obj to local variable o and set obj = null. That removes the Activity field’s reference, but the Runnable still captures o. Calling an Activity instance method can also capture the Activity itself. The queue can retain them until execution or cancellation. A Handler executes on its Looper’s thread; it does not automatically move work to a background thread.
This complete class demonstrates a delayed title update needed only by visible UI. Call showTitleLater() on the UI thread while the page is visible. New work replaces the previous request, and onStop() removes the pending callback:
1 | import android.app.Activity; |
Cleanup removes the specific Runnable owned by this page, without deleting unrelated work on a shared Handler. removeCallbacks() cannot interrupt work that has already started. Removal scans the queue; this example is intended for a small number of page-owned tasks. Persistent background work needs its own lifetime and cancellation policy. See the Handler API.
Scenario B: Listeners and Subscriptions
The old LockScreen/PhoneStateListener example illustrates a longer-lived owner retaining a shorter-lived object. That is a useful risk to examine, but forgetting to unregister does not establish an inevitable system_ui OOM. The Android version, listener implementation, and actual strong-reference chain require inspection.
PhoneStateListener was deprecated in Android 12 (API 31). New code uses TelephonyCallback with paired registerTelephonyCallback() and unregisterTelephonyCallback() calls. The current registration contract says the framework retains the callback weakly, so the caller must retain it during the subscription; the old listener’s ownership model cannot simply be carried forward. Meet the permissions for the chosen events, and unregister the same instance when its subscription ends. See PhoneStateListener and TelephonyManager.
5. Releasing Resources in Lifecycle Callbacks
Match release to acquisition and purpose. A listener needed only while visible can follow onStart()/onStop(), while interaction-only work can follow onResume()/onPause(). In multi-window scenarios from API 24, a paused Activity can remain visible.
onDestroy() is not guaranteed before the system terminates a process. Do not defer all persistent state saving until then or perform large synchronous I/O in lifecycle callbacks. Separate data retained across configuration changes from references to one particular Activity/View instance. See the Activity lifecycle and process lifecycle.
6. Eliminating Overdraw
Overdraw means drawing a pixel more than once in a frame. Text over a background can be necessary; remove redundant backgrounds and covering layers that contribute no visual result, rather than removing all backgrounds.
Extra drawing can increase GPU work and memory-bandwidth demand, but a deep View tree does not mean every layer covers the same pixels. Eliminating one draw does not promise a proportional reduction in Bitmap or process-heap usage. Evaluate clipping, transparency, actual frame timing, and GPU evidence.
See this blog’s Overdraw and Its Optimization and the official Reduce overdraw guide.
7. Using System Resources
Public system strings, drawables, styles, and layouts can replace duplicate app resources when their meaning and appearance fit. Their appearance can vary with system version, theme, or device, so inspect target devices and avoid hidden internal resources.
For example, this layout uses the public android:id/list identifier:
1 | <ListView xmlns:android="http://schemas.android.com/apk/res/android" |
That ID is not a shared ListView object. The app still instantiates the View. Using a system ID alone does not establish a runtime memory saving. Check packaged resources, APK size, and runtime allocations separately. See Access platform resources.
8. Using Memory Profiling Tools
It is hard to get memory management right in one pass. At each stage of development, check screen entry/exit, scrolling, image loading, and foreground/background transitions. Look for objects surviving beyond their intended lifetime, not merely a high number at one instant.
Android Studio heap dumps expose Java/Kotlin objects and reference paths. MAT remains useful for compatible HPROF files, while native and graphics memory need additional tools. Lint gives static clues; it does not prove the absence of leaks.
For bitmap investigations, also consider the version limits in this blog’s Restoring Bitmap Objects from MAT. A native address alone cannot reconstruct pixel data. Recheck allocations, retention, and user experience through the original scenario after each fix.
About Me && Blog
Here are my introduction and related links. I welcome conversations with fellow developers and opportunities to learn from one another.
- About me: contact details and WeChat groups.
- Blog navigation: a guide to this blog’s articles.
- Android Performance Optimization: Essential Skills and Tools: recommendations and contributions are welcome through WeChat.
- Android Performance community: The paid community stopped operating in 2026 and no longer accepts new members; this link preserves its history.
If you want to go fast, go alone. If you want to go far, go together.
