Is Your Bitmap Optimization Ready for Android 17’s New MemoryLimiter?
Android 17 introduces MemoryLimiter, a system‑level app memory cap that silently kills processes exceeding device‑based limits, making unoptimized Bitmaps the biggest risk; the article explains the mechanism, detection methods, and five concrete Bitmap‑optimisation strategies plus new Android Studio tools to keep apps alive.
MemoryLimiter: Android 17’s system‑level app memory limit
Android 17 introduces App Memory Limits , a per‑app RAM cap derived from the device’s total memory. The system component MemoryLimiter enforces the limit.
If an app’s runtime memory (AnonSwap) exceeds the calculated limit, Android terminates the process silently. The exit reason is REASON_OTHER with description "MemoryLimiter:AnonSwap" , and no crash stack is produced.
Detection
Online detection via ActivityManager.getHistoricalProcessExitReasons:
val exitInfos = activityManager.getHistoricalProcessExitReasons(packageName, 0, 10)
for (info in exitInfos) {
if (info.reason == ApplicationExitInfo.REASON_OTHER &&
info.description?.contains("MemoryLimiter") == true) {
Log.w("Memory", "App killed by MemoryLimiter: ${info.description}")
}
}Local debugging with adb commands (Android 17 provides three commands):
# Show current status
adb shell am memory-limiter status
# Ignore a specific UID (useful for development)
adb shell am memory-limiter ignore <uid>
# Manually set a limit for a PID (MB)
adb shell am memory-limiter manual <pid> 256
# Reset to default
adb shell am memory-limiter manual <pid> noneThe TRIGGER_TYPE_ANOMALY trigger can capture a heap dump just before a MemoryLimiter kill.
Why Bitmaps dominate memory usage
Under MemoryLimiter pressure, Bitmap objects are the most likely to exceed the per‑app quota. As stated at Google I/O, “Bitmaps are the largest common memory objects your app will have to deal with.”
Bitmap memory accounting
Memory consumption = width × height × bytes‑per‑pixel. The default ARGB_8888 config uses 4 bytes per pixel.
100 KB JPEG → 1000×1000 view → 4 MB in ARGB_8888
4K photo (3840×2160) → ~33 MB
2000×2000 social image (~500 KB) → 16 MB
Risk example
// Assume a 512 MB app limit
// Loading 20 high‑res images (2000×1500 × 4 bytes ≈ 12 MB each)
// → 240 MB just for images, nearly half the quota.
// Adding duplicate Bitmaps or unreleased old images can trigger a silent kill.Bitmap decoding and scaling sit on the frame‑rendering critical path, and higher screen densities increase the risk.
Five practical Bitmap‑optimisation strategies
1. Downsampling (avoid feeding a 4K image to a thumbnail)
Read dimensions only, compute an appropriate inSampleSize (power of two), then decode the reduced image.
// Step 1: read dimensions only
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(filePath, options)
// Step 2: compute sample size
options.inSampleSize = calculateInSampleSize(options, targetWidth, targetHeight)
// Step 3: decode down‑sampled bitmap
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeFile(filePath, options)Setting inSampleSize = 4 reduces memory to 1/16 of the original.
Library‑based approach (recommended):
// Coil (Compose‑friendly)
AsyncImage(
model = ImageRequest.Builder(context)
.data(url)
.size(100, 100) // auto‑scale
.build(),
contentDescription = null
)
// Glide
Glide.with(context)
.load(url)
.override(100, 100) // auto‑downsample
.into(imageView)2. Cropping (avoid padding pixels)
Incorrect: creating a 1000×1000 Bitmap when the actual content is 800×600, leaving large transparent borders.
Correct approaches:
Use InsetDrawable to add padding at draw time.
Set padding on the View.
In Compose, apply Modifier.padding.
// InsetDrawable
val insetDrawable = InsetDrawable(bitmapDrawable, left, top, right, bottom)
imageView.setImageDrawable(insetDrawable)
// View padding
imageView.setPadding(left, top, right, bottom)
// Compose
Image(painter = painterResource(id = R.drawable.photo), modifier = Modifier.padding(16.dp))3. Pixel format (ARGB_8888 vs RGB_565)
ARGB_8888uses 4 bytes/pixel; RGB_565 uses 2 bytes, halving memory. Use RGB_565 for thumbnails and non‑transparent images.
// Glide example
Glide.with(context)
.load(url)
.format(DecodeFormat.PREFER_RGB_565)
.into(thumbnailView)
// Coil example
val request = ImageRequest.Builder(context)
.data(url)
.bitmapConfig(Bitmap.Config.RGB_565)
.build()⚠️ RGB_565 can introduce banding in gradients; avoid for high‑quality visuals.
4. Vector drawables (replace small icons with vectors)
VectorDrawables and ShapeDrawables occupy zero bitmap memory, are density‑independent, and are usually only a few KB.
<!-- res/drawable/ic_heart.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#FF0000"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 ..."/>
</vector>5. Bitmap pool (reduce allocations and GC pressure)
Frequent Bitmap creation triggers GC pauses that cause UI jank. A BitmapPool reuses memory buffers instead of discarding them.
// Manual reuse with inBitmap
val reusableOptions = BitmapFactory.Options().apply {
inMutable = true
inBitmap = existingBitmap // reuse memory
}
val newBitmap = BitmapFactory.decodeFile(path, reusableOptions)Glide and Coil manage a BitmapPool automatically, which is why using these libraries is recommended over manual handling.
Android Studio tools for Bitmap problems
Duplicate Bitmap detection
The Profiler can highlight duplicate Bitmaps with a yellow triangle.
Open Android Studio → Profiler panel.
Select “Analyze Memory Usage” (Heap Dump).
Start the snapshot capture.
Look for the yellow‑triangle warning in the results or filter by “Duplicate Bitmaps”.
Click a marked item → Bitmap Preview to see the actual image.
Investigate cache strategy, RecyclerView reuse, or cross‑component loading.
LeakCanary integration in Android Studio Panda
LeakCanary is bundled as a dedicated Profiler task, requiring no extra dependency.
Comparison:
Traditional: add LeakCanary library, run on device, manual source navigation.
Android Studio Panda: built‑in, runs on the development machine, one‑click “Jump To Source”, AI‑assisted analysis export.
ProfilingManager – event‑driven heap‑dump triggers
Two triggers are available: TRIGGER_TYPE_OOM: captures a heap dump at the exact moment of an OutOfMemoryError. TRIGGER_TYPE_ANOMALY: when a severe performance anomaly (e.g., imminent MemoryLimiter kill) is detected, a heap dump is collected before termination.
val profilingManager = applicationContext.getSystemService(ProfilingManager::class.java)
val triggers = arrayListOf(
ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_ANOMALY).build()
)
profilingManager.registerForAllProfilingResults(executor) { result ->
if (result.errorCode == ProfilingResult.ERROR_NONE) {
uploadHeapDump(result.resultFilePath)
}
}
profilingManager.addProfilingTriggers(triggers)Collected heap dumps can be opened in Perfetto UI’s Heap Dump Explorer for flame‑graph analysis.
Additional strategies
onTrimMemory – proactive release when UI is hidden
Implement ComponentCallbacks2 and clear large caches when the app receives TRIM_MEMORY_UI_HIDDEN or TRIM_MEMORY_BACKGROUND.
class MyApp : Application(), ComponentCallbacks2 {
override fun onTrimMemory(level: Int) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
imageLoader.memoryCache?.clear()
}
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
database.close()
}
}
}From Android 14 onward only these two levels are sent; older constants are deprecated.
R8 optimisation – shrink bytecode to reduce runtime memory
Enable R8 with the optimized ProGuard file to compress class, method, and field names, remove unused code/resources, and shrink the resident code footprint.
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}Using the older proguard-android.txt prevents these optimisations and is no longer supported in AGP 9.
Action checklist for the MemoryLimiter era
P0 – Use Glide/Coil : automatic scaling, pooling and caching reduces image memory by 50‑90%.
P0 – Enable full R8 optimisation : lowers overall memory baseline.
P1 – Use RGB_565 for thumbnails : halves image memory.
P1 – Run Profiler duplicate‑Bitmap detection : eliminates unnecessary copies.
P1 – Upgrade to Android Studio Panda with LeakCanary : speeds leak discovery and fixes.
P2 – Implement onTrimMemory callbacks : releases non‑critical memory.
P2 – Integrate ProfilingManager triggers : captures OOM or anomaly heap dumps for remote analysis.
P2 – Test locally with adb memory-limiter : validates memory safety before release.
Conclusion
MemoryLimiter is already active in Android 17 beta builds and will roll out to billions of devices. Because Bitmaps dominate an app’s memory footprint, optimising them is the most effective defence. The upgraded toolchain—Android Studio Profiler, built‑in LeakCanary, and ProfilingManager—makes detection and remediation faster, while R8 reduces the code‑base memory overhead. Applying these measures keeps apps alive and performant under the new memory constraints.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
AndroidPub
Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
