Skip to content

Commit 5365d9c

Browse files
committed
Share and release marker bitmaps in mapsforge layer (rel. to cgeo#18404)
Every marker created a private ARGB_8888 copy of its source bitmap via AndroidGraphicFactory.convertToBitmap(), although all caches of one type share the same source bitmap from MapMarkerUtils' marker cache. Those copies were never released either: MapsforgeV6ZLevelGroupLayer dropped removed layers without calling Layer.onDestroy(), so Marker.onDestroy() never decremented the bitmap reference count and Mapsforge's reusable bitmap pool was never refilled. Share one Mapsforge bitmap per source bitmap and hand out reference counted references, and destroy layers when they are removed from the group layer or when the group layer itself is destroyed.
1 parent 767d74e commit 5365d9c

2 files changed

Lines changed: 97 additions & 3 deletions

File tree

main/src/main/java/cgeo/geocaching/unifiedmap/geoitemlayer/MapsforgeV6GeoItemLayer.java

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import androidx.annotation.NonNull;
1919

2020
import java.util.Collection;
21+
import java.util.LinkedHashMap;
2122
import java.util.List;
23+
import java.util.Map;
2224

2325
import org.mapsforge.core.graphics.Paint;
2426
import org.mapsforge.core.graphics.Style;
@@ -41,6 +43,43 @@ public class MapsforgeV6GeoItemLayer implements IProviderGeoItemLayer<int[]> {
4143

4244
private MapsforgeV6ZLevelGroupLayer groupLayer;
4345

46+
private final MarkerBitmapCache markerBitmaps = new MarkerBitmapCache();
47+
48+
/**
49+
* Mapsforge needs its own copy of every marker bitmap, but many markers share the same source bitmap
50+
* (all caches of one type look alike). Creating one copy per marker wastes a full ARGB_8888 bitmap each
51+
* time, so copies are shared and released via Mapsforge's reference counting instead.
52+
* <br>
53+
* Reference counting contract: a freshly created bitmap has a count of 0, meaning "one owner" - which is
54+
* this cache. Every marker handed the bitmap increments, {@link Marker#onDestroy()} decrements again, and
55+
* the bitmap is only destroyed once the count drops below 0.
56+
*/
57+
private static final class MarkerBitmapCache extends LinkedHashMap<Bitmap, org.mapsforge.core.graphics.Bitmap> {
58+
59+
private static final long serialVersionUID = 1L;
60+
private static final int MAX_ENTRIES = 256;
61+
62+
MarkerBitmapCache() {
63+
super(16, 0.75f, true);
64+
}
65+
66+
@Override
67+
protected boolean removeEldestEntry(final Map.Entry<Bitmap, org.mapsforge.core.graphics.Bitmap> eldest) {
68+
if (size() <= MAX_ENTRIES) {
69+
return false;
70+
}
71+
eldest.getValue().decrementRefCount(); // give up this cache's own reference
72+
return true;
73+
}
74+
75+
void release() {
76+
for (org.mapsforge.core.graphics.Bitmap bitmap : values()) {
77+
bitmap.decrementRefCount();
78+
}
79+
clear();
80+
}
81+
}
82+
4483
public MapsforgeV6GeoItemLayer(final MapView mapView) {
4584
this.mapView = mapView;
4685
this.layerManager = mapView.getLayerManager();
@@ -76,6 +115,9 @@ public void destroy(final Collection<Pair<GeoPrimitive, int[]>> values) {
76115
}
77116
groupLayer.requestRedraw();
78117
}
118+
// the markers created from these bitmaps have been destroyed above, so releasing our own
119+
// reference now actually frees them
120+
markerBitmaps.release();
79121
this.mapView = null;
80122
}
81123

@@ -144,7 +186,7 @@ public String onMapChangeBatchEnd(final long processedCount) {
144186
return null;
145187
}
146188

147-
private static Marker createMarker(final Geopoint point, final GeoIcon icon) {
189+
private Marker createMarker(final Geopoint point, final GeoIcon icon) {
148190
if (point == null || icon == null || icon.getBitmap() == null) {
149191
return null;
150192
}
@@ -155,13 +197,29 @@ private static Marker createMarker(final Geopoint point, final GeoIcon icon) {
155197

156198
final Marker newMarker = new Marker(
157199
latLong(point),
158-
AndroidGraphicFactory.convertToBitmap(new BitmapDrawable(CgeoApplication.getInstance().getResources(), bitmap)),
200+
getMarkerBitmap(bitmap),
159201
(int) ((-icon.getXAnchor() + 0.5f) * bitmap.getWidth()),
160202
(int) ((-icon.getYAnchor() + 0.5f) * bitmap.getHeight()));
161203
newMarker.setBillboard(!icon.isFlat());
162204
return newMarker;
163205
}
164206

207+
/**
208+
* Gets the Mapsforge representation of the given bitmap, creating it on first use.
209+
* The returned bitmap carries one additional reference for the marker it is about to be handed to.
210+
*/
211+
private org.mapsforge.core.graphics.Bitmap getMarkerBitmap(final Bitmap bitmap) {
212+
org.mapsforge.core.graphics.Bitmap markerBitmap = markerBitmaps.get(bitmap);
213+
if (markerBitmap == null) {
214+
// do NOT wrap the source bitmap directly: it is owned by MapMarkerUtils' marker cache and would
215+
// be recycled by Mapsforge once the last marker using it is destroyed
216+
markerBitmap = AndroidGraphicFactory.convertToBitmap(new BitmapDrawable(CgeoApplication.getInstance().getResources(), bitmap));
217+
markerBitmaps.put(bitmap, markerBitmap);
218+
}
219+
markerBitmap.incrementRefCount();
220+
return markerBitmap;
221+
}
222+
165223
private static Paint createPaint(@ColorInt final int color) {
166224
final Paint p = AndroidGraphicFactory.INSTANCE.createPaint();
167225
p.setColor(color);

main/src/main/java/cgeo/geocaching/unifiedmap/geoitemlayer/MapsforgeV6ZLevelGroupLayer.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import androidx.annotation.Nullable;
44

5+
import java.util.ArrayList;
56
import java.util.HashMap;
7+
import java.util.List;
68
import java.util.Map;
79
import java.util.SortedMap;
810
import java.util.TreeMap;
@@ -62,6 +64,7 @@ public void remove(final boolean redraw, @Nullable final int ... context) {
6264
if (context == null || context.length < 2) {
6365
return;
6466
}
67+
final List<Layer> removedLayers = new ArrayList<>(context.length);
6568
layerLock.lock();
6669
try {
6770
final int zLevel = context[0];
@@ -73,19 +76,52 @@ public void remove(final boolean redraw, @Nullable final int ... context) {
7376
if (context[i] == 0) {
7477
continue;
7578
}
76-
zLevelMap.remove(context[i]);
79+
final Layer removed = zLevelMap.remove(context[i]);
80+
if (removed != null) {
81+
removedLayers.add(removed);
82+
}
7783
}
7884
if (zLevelMap.isEmpty()) {
7985
this.layerItemMap.remove(zLevel);
8086
}
8187
} finally {
8288
layerLock.unlock();
8389
}
90+
// free resources held by the removed layers (esp. bitmaps of markers). Do this outside of our
91+
// own lock so we never call foreign code while holding it
92+
destroyLayers(removedLayers);
8493
if (redraw) {
8594
requestRedraw();
8695
}
8796
}
8897

98+
/** removes all layers from this group and frees the resources held by them */
99+
@Override
100+
public void onDestroy() {
101+
final List<Layer> removedLayers = new ArrayList<>();
102+
layerLock.lock();
103+
try {
104+
for (Map<Integer, Layer> zLevelMap : this.layerItemMap.values()) {
105+
removedLayers.addAll(zLevelMap.values());
106+
}
107+
this.layerItemMap.clear();
108+
} finally {
109+
layerLock.unlock();
110+
}
111+
destroyLayers(removedLayers);
112+
super.onDestroy();
113+
}
114+
115+
/**
116+
* Mapsforge layers hold resources which are only released on onDestroy(); for a Marker this is the
117+
* reference to its bitmap. Removing a layer without destroying it therefore leaks that bitmap.
118+
*/
119+
private static void destroyLayers(final Iterable<Layer> layers) {
120+
for (Layer layer : layers) {
121+
layer.onDestroy();
122+
}
123+
}
124+
89125
@Override
90126
public void draw(final BoundingBox boundingBox, final byte zoomLevel, final Canvas canvas, final Point topLeftPoint, final Rotation rotation) {
91127
layerLock.lock();

0 commit comments

Comments
 (0)