CN

Android Code Memory Optimization Suggestions - Android Resources

Word count: 2.4kReading time: 14 min
2015/07/20
loading

Android Memory Optimization Series:

  1. Android Code Memory Optimization Suggestions - Android (Official)
  2. Android Code Memory Optimization Suggestions - Java (Official)
  3. Android Code Memory Optimization Suggestions - Android Resources
  4. 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
2
3
4
5
private static void clearImageViewImage(android.widget.ImageView imageView) {
if (imageView != null) {
imageView.setImageDrawable(null);
}
}

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
2
3
4
5
private static void clearViewBackground(android.view.View view) {
if (view != null) {
view.setBackground(null);
}
}

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.

Original image on the left; selected-region stretching on the right

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:

  1. Set inJustDecodeBounds = true to read dimensions and type without allocating the full pixels. Check that outWidth and outHeight are valid before using them.
  2. Compute inSampleSize for 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;

public final class QueryExample {
public static int countRows(ContentResolver resolver, Uri uri,
String[] projection) {
try (Cursor cursor = resolver.query(uri, projection, null, null, null)) {
if (cursor == null) {
throw new IllegalStateException("Provider returned no cursor");
}
return cursor.getCount();
}
}
}

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.

ListView row-reuse illustration

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;

public final class LabelsAdapter extends BaseAdapter {
private final LayoutInflater inflater;
private final List<String> labels;

public LabelsAdapter(Context context, List<String> labels) {
inflater = LayoutInflater.from(context);
this.labels = new ArrayList<>(labels);
}

@Override public int getCount() { return labels.size(); }
@Override public String getItem(int position) { return labels.get(position); }
@Override public long getItemId(int position) { return position; }

@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView label;
if (convertView == null) {
convertView = inflater.inflate(
android.R.layout.simple_list_item_1, parent, false);
label = (TextView) convertView.findViewById(android.R.id.text1);
convertView.setTag(label);
} else {
label = (TextView) convertView.getTag();
}
label.setText(getItem(position));
return convertView;
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import android.app.Activity;
import android.os.Handler;
import android.os.Looper;

public final class DeferredTitleActivity extends Activity {
private final Handler uiHandler = new Handler(Looper.getMainLooper());
private Runnable pendingTitle;

public void showTitleLater(final String title, long delayMillis) {
cancelPendingTitle();
pendingTitle = new Runnable() {
@Override public void run() {
pendingTitle = null;
setTitle(title);
}
};
if (!uiHandler.postDelayed(pendingTitle, delayMillis)) {
pendingTitle = null;
}
}

private void cancelPendingTitle() {
if (pendingTitle != null) {
uiHandler.removeCallbacks(pendingTitle);
pendingTitle = null;
}
}

@Override protected void onStop() {
cancelPendingTitle();
super.onStop();
}
}

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
2
3
4
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />

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.

  1. About me: contact details and WeChat groups.
  2. Blog navigation: a guide to this blog’s articles.
  3. Android Performance Optimization: Essential Skills and Tools: recommendations and contributions are welcome through WeChat.
  4. 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.

Scan with WeChat

CATALOG
  1. 1. Android Resource Optimization
    1. 1.1. 1. Bitmap Optimization
      1. 1.1.1. 1.1 Manually Releasing Bitmap Resources
      2. 1.1.2. 1.2 Releasing ImageView Images
      3. 1.1.3. 1.3 Releasing ImageView Backgrounds
      4. 1.1.4. 1.4 Use Nine-Patch for Suitable Stretchable Backgrounds
      5. 1.1.5. 1.5 Decode Large Images for the Target Size
    2. 1.2. 2. Unclosed Database Cursors
    3. 1.3. 3. Not Recycling convertView in Adapters
    4. 1.4. 4. Releasing Object References
    5. 1.5. 5. Releasing Resources in Lifecycle Callbacks
    6. 1.6. 6. Eliminating Overdraw
    7. 1.7. 7. Using System Resources
    8. 1.8. 8. Using Memory Profiling Tools
  2. 2. About Me && Blog