Android Oreo中Bitmap内存分配与内存排查技术问询
Background
In previous Android versions, we could get heap memory usage with this code:
@JvmStatic fun getHeapMemStats(context: Context): String { val runtime = Runtime.getRuntime() val maxMemInBytes = runtime.maxMemory() val availableMemInBytes = runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory()) val usedMemInBytes = maxMemInBytes - availableMemInBytes val usedMemInPercentage = usedMemInBytes * 100 / maxMemInBytes return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " + Formatter.formatShortFileSize(context, maxMemInBytes) + " (" + usedMemInPercentage + "%)" }
The more memory used (especially when storing Bitmaps), the closer we get to the app's heap limit, which triggers an OutOfMemoryError (OOM) once reached.
Problem Observations
On Android O (8.1, we suspect 8.0 is also affected), the above code doesn't reflect Bitmap allocations—Android Profiler shows Bitmap memory is accounted for in native memory. We verified this with the test code below:
val list = ArrayList<Bitmap>() Log.d("AppLog", "memStats:" + MemHelper.getHeapMemStats(this)) useMoreMemoryButton.setOnClickListener { AsyncTask.execute { for (i in 0..1000) { // list.add(Bitmap.createBitmap(20000, 20000, Bitmap.Config.ARGB_8888)) list.add(BitmapFactory.decodeResource(resources, R.drawable.huge_image)) Log.d("AppLog", "heapMemStats:" + MemHelper.getHeapMemStats(this) + " nativeMemStats:" + MemHelper.getNativeMemStats(this)) } } }
We found the app's memory usage far exceeds the heap limit (201MB), with several abnormal behaviors:
- When getting native memory stats with the code below, the native heap limit changes dynamically—we can't determine the real upper bound to set cache sizes:
@JvmStatic fun getNativeMemStats(context: Context): String { val nativeHeapSize = Debug.getNativeHeapSize() val nativeHeapFreeSize = Debug.getNativeHeapFreeSize() val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " + Formatter.formatShortFileSize(context, nativeHeapSize) + " (" + usedMemInPercentage + "%)" }
Logs show native memory usage keeps growing, but the limit constantly shifts; when no more Bitmaps can be stored, the app closes without a crash prompt.
2. When creating large Bitmaps with Bitmap.createBitmap(20000, 20000, Bitmap.Config.ARGB_8888), logs show abnormally large native memory values, eventually triggering a NullPointerException (NPE) instead of OOM. The Profiler memory curve doesn't rise noticeably, drops sharply on crash, and GC runs frequently.
3. Bitmaps can't be previewed in memory dumps.
Technical Questions
- What specific memory management changes happened in Android O and why? How does Bitmap allocation work now?
- Can we preview Bitmaps in memory dumps on Android O?
- How can we get the maximum native memory allowed for the app to make cache size decisions?
- Are there any videos or articles explaining Bitmap allocation and OOM handling for this Android version?
- Does this change affect cache libraries that rely on heap memory size?
- Why can we create far more 20000x20000 Bitmaps than decode 7680x7680 real images? Is memory compression involved?
- Why is there such a huge difference in native memory stats between the two Bitmap operations?
- Why does decoding Bitmaps not show any error prompts, while creating Bitmaps triggers an NPE?
- Can Google Play and Crashlytics detect crashes caused by this kind of memory issue? How can we sense these problems during development or from users?
Answers
Let's break down each question with practical explanations based on Android O's memory model changes:
1. Android O Memory Management Changes & Bitmap Allocation
Starting with Android O, Bitmaps are allocated in native memory instead of the Dalvik/ART heap—this was a key change to reduce GC pressure on the app heap. Previously, large Bitmap allocations triggered frequent garbage collection and OOM errors tied strictly to heap limits, even when there was plenty of free native memory available.
Now, the system uses a shared native memory pool for Bitmaps, managed by the ART runtime. When you create a Bitmap, only a small metadata wrapper (holding width, height, config, etc.) lives in the app heap; the actual pixel data resides in native memory. This decouples Bitmap memory from the app's heap limit, letting the system utilize available system memory more efficiently and improve app stability.
2. Previewing Bitmaps in Memory Dumps
Unfortunately, you can't directly preview Bitmaps in standard heap dumps (hprof files) on Android O. Heap dumps only capture the small Bitmap wrapper objects in the ART heap, not the pixel data stored in native memory.
To inspect Bitmap memory, use Android Studio Profiler's Memory tab—it can visualize native memory usage and show Bitmap previews by querying the native memory pool directly, as long as the app is running and connected to the profiler.
3. Getting Native Memory Upper Limit
There's no direct API to get a fixed native memory limit for your app on Android O. The native heap size (Debug.getNativeHeapSize()) is dynamic because the system adjusts it based on available system memory, background app activity, and other system-level factors.
Instead of relying on a fixed limit, use adaptive caching strategies:
- Use
ActivityManager.getMemoryClass()to get the app's heap limit as a baseline for overall memory budget - Monitor native memory usage over time and adjust cache sizes when you see signs of pressure (like frequent GC, or system-level memory warnings via
onTrimMemory()callbacks) - Configure
LruCachebased on a percentage of the total estimated available memory (useActivityManager.getLargeMemoryClass()if your app uses thelargeHeapflag, but note this still isn't a hard limit for native memory)
4. Resources for Bitmap Allocation & OOM Handling
While there aren't resources specifically focused on Android O's Bitmap changes, these official materials cover the core applicable concepts:
- Android Developer Docs' Managing Bitmap Memory guide (covers native allocation best practices)
- Google I/O session: "Memory Management for Android Apps" (explains ART memory model changes including native Bitmap allocation)
- Android Performance Patterns video series: Episodes on Bitmap optimization and memory monitoring (covers how to avoid OOMs with native-allocated Bitmaps)
5. Impact on Cache Libraries Relying on Heap Size
Yes, this change can affect cache libraries that calculate their size based on the app's heap limit (like older LruCache setups using Runtime.getRuntime().maxMemory()). Since Bitmap memory no longer counts against the heap, these libraries might allow more cache entries than the system can handle, leading to native memory exhaustion.
To address this:
- Update cache configurations to account for both heap and native memory usage
- Switch to libraries that support native-aware caching, or modify existing ones to monitor native memory stats alongside heap stats
- Use
onTrimMemory()to evict cache entries when the system signals memory pressure
6. More createBitmap Instances vs Decoded Images
When you call Bitmap.createBitmap(20000, 20000, ARGB_8888), the Bitmap is created with uninitialized pixel data. The system optimizes this by using lazy allocation—kernel-level pages aren't actually allocated until you write pixel data to them. This lets you create more empty Bitmaps before hitting physical memory limits.
In contrast, decoding a real image with BitmapFactory.decodeResource() loads actual pixel data into native memory immediately, taking up the full expected size. Real images also have additional overhead from decoding buffers, which adds to their memory footprint.
7. Native Memory Stats Difference Between the Two Operations
The abnormal native memory values from Bitmap.createBitmap come down to how the system reports memory: Debug.getNativeHeapSize() returns the virtual memory address space allocated, not the actual physical memory (RSS) in use. For empty Bitmaps, the virtual size is large, but physical memory isn't consumed until pixel data is written.
Decoding real images uses physical memory immediately, so the native stats reflect the actual memory consumed. This is why you see such a big discrepancy between the two operations.
8. NPE Instead of OOM When Creating Bitmaps
When you try to create an extremely large Bitmap (20000x20000), the system can't allocate the required native memory. Instead of throwing an OOM, createBitmap may return null (a quirk of how ART handles native allocation failures for Bitmaps). When you add this null Bitmap to your ArrayList and attempt to use it later, you get an NPE.
For BitmapFactory.decodeResource(), the method may also return null on allocation failure, but if you're just adding it to the list without immediate use, the app might be killed by the system (a low-memory kill) instead of crashing with an exception—hence the silent closure.
9. Detecting These Memory Issues with Google Play & Crashlytics
- Google Play Console: It detects low-memory kills (LMKs)—when your app is terminated by the system due to excessive native memory usage, this will appear in the "Crashes & ANRs" section under "Low Memory Kills". It also tracks ANRs caused by memory pressure.
- Crashlytics: By default, it doesn't catch LMKs (since the app is terminated without throwing an exception), but you can implement custom monitoring:
- Track native memory usage and log warnings when it approaches critical levels
- Use
Application.registerActivityLifecycleCallbacks()to detect unexpected app termination - Catch
NullPointerExceptioncases related to Bitmaps and tag them as memory-related issues
During development:
- Use Android Studio Profiler's Memory tab to monitor native memory in real-time
- Enable StrictMode to detect memory leaks
- Test on low-RAM devices to simulate memory pressure
- Run
adb shell dumpsys meminfo <package-name>to get detailed memory stats (including native memory RSS)
内容的提问来源于stack exchange,提问作者android developer

