Android 17 MemoryLimiter Is Here—Have You Optimized Your Bitmaps?
Android 17 introduces a system‑level MemoryLimiter that silently kills apps exceeding per‑device RAM limits, making bitmap memory the primary optimization target; the article explains the mechanism, detection methods, new Android Studio tools, and five concrete strategies to keep your app alive.
MemoryLimiter Overview
Android 17 adds a system component called MemoryLimiter that enforces app‑wide memory limits based on the device’s total RAM. When an app’s anonymous swap usage exceeds the calculated limit, the OS terminates the process silently—no crash stack, no OutOfMemoryError, exit reason REASON_OTHER and description "MemoryLimiter:AnonSwap".
Why Google Introduced It
The change serves two core goals: (1) prevent a single misbehaving app (e.g., severe memory leak with a foreground service) from forcing the Low Memory Killer to kill many healthy background apps, and (2) protect multitask experience and user state by avoiding slow cold starts, lost scroll positions, and extra CPU/battery load when the system must clear cached apps.
How It Works
The runtime flow is simple: device total RAM → compute per‑app memory ceiling → if AnonSwap > limit, MemoryLimiter kills the process with the silent reason described above. Google describes the limits as conservative for extreme leak scenarios, but they will tighten in future releases.
Detecting MemoryLimiter Kills
Online detection:
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:
# View current status
adb shell am memory-limiter status
# Ignore a UID (useful for testing)
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> noneUsing the TRIGGER_TYPE_ANOMALY trigger, the system can automatically capture a heap dump just before a MemoryLimiter termination, providing a precise memory snapshot for analysis.
Why Bitmap Is the Biggest Threat
Bitmaps dominate an app’s memory footprint. Examples from the I/O talk:
100 KB JPEG decoded to a 1000×1000 view → 4 MB in memory.
~3 MB 4K photo (3840×2160) → 33 MB .
2000×2000 social image (~500 KB) → 16 MB .
The calculation is width × height × bytesPerPixel. With the default ARGB_8888 format each pixel costs 4 bytes.
Risks Under MemoryLimiter
// Assume a 512 MB per‑app limit
// Loading 20 high‑res images (2000×1500, 4 bytes each)
// 20 × (2000×1500×4) ≈ 240 MB → almost half the quota
// Add duplicate caches and unreleased bitmaps → silent killBecause bitmap decoding and scaling sit on the UI rendering critical path, the risk grows as screen densities increase (2K, 4K displays).
Five Practical Bitmap‑Optimization Strategies
1. Downsampling (Never feed a 4K image to a thumbnail)
Read dimensions only, compute an appropriate inSampleSize (power of two), then decode the reduced bitmap. The memory reduction is the square of the sample factor (e.g., inSampleSize = 4 → memory ↓ to 1/16).
// Step 1: read bounds 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 with sampling
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeFile(filePath, options)Libraries handle this automatically: Coil (Compose‑friendly) and Glide both provide .size() or .override() to downsample.
2. Cropping (Avoid padding inside the bitmap)
Instead of embedding transparent borders, use InsetDrawable or view 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 Config (ARGB_8888 → RGB_565)
Switching to RGB_565 halves per‑pixel memory (2 bytes vs 4 bytes). Ideal for thumbnails and list items that don’t need alpha. Beware of banding in gradient‑rich 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()4. Vector Drawables (Replace simple icons with vectors)
VectorDrawable or ShapeDrawable consume zero bitmap memory, are density‑independent, and are only a few kilobytes of 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 (Reuse instead of reallocating)
Frequent bitmap creation triggers GC pauses that cause UI jank. Reuse via inBitmap or rely on Glide/Coil’s built‑in pool.
// Manual reuse
bitmap.recycle()
val reusableOptions = BitmapFactory.Options().apply {
inMutable = true
inBitmap = existingBitmap // reuse memory
}
val newBitmap = BitmapFactory.decodeFile(path, reusableOptions)Glide and Coil already maintain a robust pool, which is why the platform recommends using these libraries over manual management.
Android Studio New Tools
The Profiler can now highlight duplicate bitmaps with a yellow warning triangle. Steps:
Open Android Studio → Profiler panel.
Select “Analyze Memory Usage” (Heap Dump).
Start snapshot collection.
Look for the yellow triangle or filter by “Duplicate Bitmaps”.
Click a warning to open the Bitmap Preview and trace the source code.
LeakCanary is integrated directly into Android Studio Panda as a dedicated Profiler task, requiring no extra dependency. Compared with the traditional approach, Panda offers zero‑config, on‑device analysis, one‑click “Jump To Source”, and AI‑assisted result export.
ProfilingManager Triggers for Online Dumps
TRIGGER_TYPE_OOM : automatically captures a heap dump when an OutOfMemoryError occurs.
TRIGGER_TYPE_ANOMALY : when a severe performance anomaly such as an imminent MemoryLimiter kill is detected, a heap dump is taken 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 dumps can be opened in Perfetto UI’s Heap Dump Explorer, which visualizes allocation hierarchies, retained size, shortest GC‑root paths, and includes an embedded flamegraph.
onTrimMemory Callbacks
Implement ComponentCallbacks2 to release large caches when the UI becomes hidden ( TRIM_MEMORY_UI_HIDDEN) or the app moves to background ( TRIM_MEMORY_BACKGROUND). From Android 14 onward only these two levels remain.
class MyApp : Application(), ComponentCallbacks2 {
override fun onTrimMemory(level: Int) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
// Release large bitmap caches, video buffers, etc.
imageLoader.memoryCache?.clear()
}
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Release resources that can be rebuilt later.
database.close()
}
}
}R8 Optimizations
Enabling full R8 optimization (minify, shrinkResources, and the proguard-android-optimize.txt rules) compresses class/method names, removes dead code, and reduces the resident code footprint, further lowering the baseline memory usage.
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}Action Checklist (Priorities)
P0 Use Glide/Coil for automatic down‑scaling, pooling, and caching – cuts image memory by 50‑90%.
P0 Enable full R8 optimization – reduces overall memory baseline.
P1 Adopt RGB_565 for all thumbnails – halves bitmap memory.
P1 Run Profiler duplicate‑bitmap detection – eliminates needless copies.
P1 Upgrade to Android Studio Panda with built‑in LeakCanary – speeds leak discovery and fixes.
P2 Implement onTrimMemory callbacks – proactively frees non‑critical memory.
P2 Register ProfilingManager triggers – captures live OOM/anomaly heap dumps.
P2 Test locally with adb am memory-limiter – validates memory safety before release.
Conclusion
MemoryLimiter is already active in Android 17 beta and will roll out to billions of devices. Because bitmaps are the dominant memory consumer, developers must adopt the five strategies above, leverage the upgraded Android Studio tooling, and enable R8 to stay safely under the new limits.
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.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
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.
