Android | Drawable | OutOfMemoryError
Sometimes, I face some weird problems while developing Android app. Well, most of the time debugging & solving the problem lead me to either of the following conclusion: I don’t know the detailed implementation well enough OR it is a design problem/bug of the Android/Support libraries.
Here I gonna share an interesting problem I faced once. I needed to show the logo of my app on the landing or launcher activity centered with fill width. So I simply put the logo image in ‘drawable’ directory and displayed it via a ImageView like following:
Resolution of the image was a bit high, 1600x1000. But the file size pretty small, around 123KB. Being a lazy developer, although I knew I should have provided different sizes of the image on different density-indicated folder like ‘drawable-hdpi’ , ‘drawable-xxxhdpi’, but I didn’t. I ignored the best practice and just put that in ‘drawable’ directory (we all do sometimes…right? ). I thought, size of the image is only 123KB, won’t cause a problem. Well, it wasn’t a problem as long as I ran it on Android Virtual Device (Nexus 5X with API 23). Logo was displayed properly. Everything was good.
Later, when I ran the app on my HTC One M8 handset, app was ok, but there was no image on launcher activity! A blank view was there instead. At first I didn’t bother much. Thought it might just be a rendering problem. I was more focused on the application functionality. Later when I got time, I just connected my device with adb and tried to check logcat. I didn’t find much information. I just saw a warning like:
05–18 15:12:49.769 10700–11238/com.oneous.app W/OpenGLRenderer: Bitmap too large to be uploaded into a texture (4800x3000, max=4096x4096)
Well, it was not very specific and also it was confusing. What it indicates about a unnamed 4800x3000 resolution image. Although I had some other images on my app but none are as large as that resolution. The largest image I used was the logo, which was: 1600x1000. So where did the warning come from?
For some other reason, I had to run the app on a GenyMotion Virtual device instance, Samsung Galaxy S6 API 21. You know what I got there? The App just crashed!! The error logs I got:
05-18 04:46:54.826 7137-7137/com.oneous.app I/art: Forcing collection of SoftReferences for 97MB allocation
05-18 04:46:54.832 7137-7137/com.oneous.app E/art: Throwing OutOfMemoryError "Failed to allocate a 102400012 byte allocation with 1048480 free bytes and 94MB until OOM"
05-18 04:46:54.834 7137-7137/com.oneous.app D/AndroidRuntime: Shutting down VM
--------- beginning of crash
05-18 04:46:54.834 7137-7137/com.oneous.app E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.oneous.app, PID: 7137
java.lang.OutOfMemoryError: Failed to allocate a 102400012 byte allocation with 1048480 free bytes and 94MB until OOM
at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
at java.lang.reflect.Method.invoke(Native Method)
Wait a minute. OutOfMemoryError… are you kidding me? I am an Advance Android developer, ok? I know the Bitmap memory consumption behavior well enough…. I know the math of calculating bitmap memory. With ARGB_8888 configuration, loading my logo image into memory will take about 6.10MB of memory (1600*1000*4 bytes). It should fit easily within per-app memory limit of Android OS. So what’s the actual problem? What did I possibly miss?
Well, at this point, I googled and saw the answers on StackOverflow and most of them were pointing the theories about loading bitmap efficiently as of link. But none helped my case until I saw an answer and immediately realize my problem. I know that behavior from the beginning of Android learning but just missed at that point. Guess what?? Well, it is how Android system handle resources according to different screen layout.
system scales your layout and drawable resources based on the current screen configuration
Oops… so in my case, as I put the image of size 1600x1000 into ‘drawable’, system try to Scale it according to density of the device. But with what ratio system scale it? For that, you have know the density ratio properly. With a baseline 48x48 resolution, ratio would be:
- 48x48 (1.0x baseline) for medium-density (mdpi)
- 72x72 (1.5x) for high-density (hdpi)
- 96x96 (2.0x) for extra-high-density (xhdpi)
- 180x180 (3.0x) for extra-extra-high-density (xxhdpi)
- 192x192 (4.0x) for extra-extra-extra-high-density (xxxhdpi)
But what is the ‘drawable’ density…I mean, what it equal with other drawable configuration qualifier? Unfortunately you won’t find enough documentation on Android Developer guide. Luckily CommonsWare do that.
res/drawable/ is a synonym for res/drawable-mdpi/, for backwards compatibility
So, for my HTC One M8 which is a xxhdpi device, system scale the image 3X times to 4800x3000 resolution. Remember the cryptic warning message about 4800x3000 resolution??
It turns more interesting on Samsung Galaxy S6 device, which is a xxxhdpi device. Here, System scale the image 4X times resulting a huge resolution 6400x4000 size image. Then to display it on ImageView, system try to allocate 6400*4000*4 bytes = 97.6MB memory and booom! OutOfMemoryError exception!! Now you can understand the error log message clearly…right?
So whats the solution if I want to stay ‘lazy’ aka want to use same file for all density devices?? Its simple. Put it on ‘drawable-nodpi’ directory. You didn’t heard of this ‘-nodpi’ configuration qualifier much, right? So do I :)
It turns out an interesting experience to me. Sometimes we ignore the best practices and forget the behavior of system and pay the price by wasting few hours debugging & googling the problem.