How I Fixed an OutOfMemory Exception Due to Custom Tiles on Google Maps in My App

Abhi
The Startup
Published in
7 min readJul 6, 2020

Hello, welcome to my first writeup. This is my experience with debugging an OOM error. I hope you learn something from it. Enjoy :)

I’ve been working on a map app for cycling from some time. A few months back I’d added a feature to see custom map tiles from Open Street Maps inside GoogleMaps. A little background on map tiles from OSM. So OSM is basically a large data set of coordinates and metadata. You’d think OSM is maps, which it is, but its essentially just a ton of data. The latest Planet OSM XML file is over 91GB in size. The maps that you see are statically pre-generated png files. Thats why you’ll notice that when you rotate OSM maps, the labels and everything rotate as well, unlike on Google Maps which are vector maps. These pngs are called tiles and are generated on a daily or weekly basis on tile servers. It is ideally possible to create these tiles on-the-go but that ends up being a costly operation and is not done for the most often seen tiles.

OSM needs to pre-render these tiles for every zoom level on a map. If OSM pre-renders all tiles for all locations for all zoom levels, it would take up a whopping 54TB of space! So they do some interesting tricks to avoid that.

To fetch these tiles, we access a url like this-
https://tile.openstreetmap.org/8/187/108.png
This represents baseurl + zoom/x/y. The x and y are not latitude longitudes but are identifiers for a tile for an area on the map using Mercator Projection. You can read about the related complex math here. Due to the massive number of tiles with increasing zoom level, OSM does not render tiles for zoom levels 20 and 21. One can do it themselves though if the space and computation power is available.

Each of these tiles are 256x256 by default. This is very low res for mobile apps and ends up looking pretty awful. So HD tiles are used which are 512x512 pngs. Here’s a comparison between the two —

Comparison between using 256x256 vs 512x512 map tiles

Now back to Android. To render these tiles, we use a map tile overlay in Google Maps. As the term says, these are just overlays on top of Google Maps itself. To show OSM tiles, I set the google map to not be shown (map becomes blank) and then set the map to show any custom map tile overlay.

googleMap.mapType = GoogleMap.MAP_TYPE_NONEgoogleMap.addTileOverlay(TileOverlayOptions().tileProvider(MyUrlTileProvider(this@MapsActivity, url)))

Since these are just overlays, you can use it on top of GoogleMaps itself to show anything. For eg, here I am showing bicycle routes on top of Google Maps and in the second picture, Strava heat map tiles with dark Google Map(a custom map style). Traffic layer works the same way. It’s just another overlay on top of the base Google Maps.

Waymarked cycling trails map tiles on normal Google Map, Strava heat map tiles on Dark Google Map

So the possibilities are endless. You can render hill shading with normal maps or show train tracks on dark maps etc etc. Rendering and showing all these kinds of maps and overlays is not the problem. The problem comes with showing many tiles (pngs) at once.

Now about the bug. A few weeks back I started noticing these OutOfMemory crashes happening in random places in the app.

The weirdest part was that they were happening at different places in the app completely unrelated to the map tiles. So pinpointing the issue became difficult. However on looking at the same event logs before the crash it was clear what was the issue.

If you’re aware of how pngs use up space on the heap, you’ll know that the size of the png is irrelevant. The space a bitmap uses in the memory is
number-of-pixels*bytes-per-pixel.

About the size of the tile pngs, the png size depends on
1. The resolution of the tile.
2. Amount of detail in the png.

So for eg, if you look at this HD tile from somewhere in the city, https://maps.wikimedia.org/osm-intl/11/1101/671@2x.png (61KB) is about 2 times bigger than a tile from the countryside https://maps.wikimedia.org/osm-intl/13/4407/2677@2x.png (23 KB). Another tile with hardly any detail is only (13 KB) https://maps.wikimedia.org/osm-intl/15/17636/10702@2x.png. So size of the tile png can highly vary.

When it comes to render these tiles on GoogleMaps using a TileProvider, the size of the bitmaps on the heap also vary.

A few tiles wont matter, but when building a route on this app (thats what my app does- a cycling route builder/planner), the user tends to look at many areas and on various zoom levels. Hundreds of tiles being loaded on the map quickly adds up in memory.

Looking at just maps around Ahmadabad, India and loading only 200 tiles added about 30MB to the heap! The longer a user uses the app and looks at more maps, it keeps adding up pretty quickly.

You must be wondering, that 200 images is nothing and that you’ve loaded many more images in your app. If you use a library like Glide, it manages your memory very well and thats why you never see an OutOfMemory exception unless you load a really high res image. However, when loading tiles on the map, its the GoogleMaps object and the TileProvider which do the memory management. I use Glide to load and cache the png but thats where its responsibility ends. GoogleMaps on the other hand, does not manage the memory well for overlay tiles. Ideally it should clear tiles which are not visible anymore, but it does not.

The OOM crash was happening on random but few devices overall. So the one thing I’m unsure of is if this crash were happening on those devices specifically because of its low RAM and aggressive memory caps for apps customised by the OEM or were they because the users on those devices loaded too many tiles.

One thing was for clear though, that google maps did not clear any tiles from the memory as you can see in this gif from the profiler.

Increasing memory occupied by Graphics as more tiles are loaded

Now onto how to clear all these tiles from memory. My first approach was to call System.gc(). I placed this call at various strategic places. However, this did not make any difference. As you can see in the gif, there are many points at which garbage collection happens on its own trigged by the system. This reduces the Native memory and Java memory, but makes no difference on the Graphics memory which continues to increase with more tiles loaded.

The second idea I had to solve this problem was use android:largeHeap="true" in the manifest. This wouldn’t be a solution really as much as mitigating the problem. And I didn’t want to add this fearing additional issues that could happen on devices in the wild.

I found the solution in this comment. googleMap.clear() was the solution. This call basically clears all overlays, all markers and polylines displayed on the map. This worked like a charm.

Graphics memory dropping after map.clear()

googleMap.clear() makes a massive difference. Before- 161.1MB and after call- 36.1 MB. Only one piece to the puzzle remains- at what point do I trigger the clear() call. The answer was onTrimMemory.

Android provides a callback to let you know when the app is running low on memory so that one can take actions to mitigate that. It also provides info on what level of memory cleanup needs to happen. Its important to use this. Simply calling your code in onTrimMemory will wreak havoc as this callback is invoked at various points in the app lifecycle. It is even called after onStop. You can read all about managing memory here.

override fun onTrimMemory(level: Int) {
when (level) {
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
//Clearing the map
googleMap.clear()

//This resets the previously shown tile overlay
setMapTileOverlay()

/*
Here I re-add the polylines and markers which were
removed from the map
*/
presenter.reshowCurrentRoute()
}
else -> {
super.onTrimMemory(level)
}
}
}

I decided to handle only CRITICAL memory scenarios which happens when the app is about to crash due to OOM. Handling MODERATE and LOW memory situation was a bad UX wherein the map and all the markers and polylines and map tiles were being re-added often and creating a janky experience.

Thanks for reading :)
Thanks to Sudhir Khanger for the idea to write a blog about this!

--

--