Bug fix: memory leak on setIcon (on Android)
Bug fix: setIcon leaking memory on Android
MODIFY
plugin.xml
plugin.xml
<?xml version='1.0' encoding='utf-8'?>
-<plugin id="cordova-plugin-googlemaps" version="2.0.0-beta3-20170828-1830" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
+<plugin id="cordova-plugin-googlemaps" version="2.0.0-beta3-20170828-2350" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
<name>cordova-plugin-googlemaps</name>
<js-module name="BaseClass" src="www/BaseClass.js">
<runs />

MODIFY
src/android/plugin/google/maps/AsyncLoadImageInterface.java
src/android/plugin/google/maps/AsyncLoadImageInterface.java
package plugin.google.maps;
public interface AsyncLoadImageInterface {
+ public void setParams(Object target, Object option, PluginAsyncInterface callback);
public void onPostExecute(AsyncLoadImage.AsyncLoadImageResult result) ;
}

MODIFY
src/android/plugin/google/maps/PluginGroundOverlay.java
src/android/plugin/google/maps/PluginGroundOverlay.java
@@ -4,6 +4,7 @@ import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
+import android.os.Bundle;
import android.util.Log;
import com.google.android.gms.maps.model.BitmapDescriptor;
@@ -11,6 +12,7 @@ import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.GroundOverlay;
import com.google.android.gms.maps.model.GroundOverlayOptions;
import com.google.android.gms.maps.model.LatLngBounds;
+import com.google.android.gms.maps.model.Marker;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
@@ -24,11 +26,12 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.Set;
public class PluginGroundOverlay extends MyPlugin implements MyPluginInterface {
- private ArrayList<AsyncTask> imageLoadingTasks = new ArrayList<AsyncTask>();
+ private HashMap<Integer, AsyncTask> imageLoadingTasks = new HashMap<Integer, AsyncTask>();
@Override
public void initialize(final CordovaInterface cordova, final CordovaWebView webView) {
@@ -304,21 +307,22 @@ public class PluginGroundOverlay extends MyPlugin implements MyPluginInterface
callbackContext.success();
}
- private void setImage_(final GroundOverlayOptions options, final String imgUrl, final PluginAsyncInterface callback) {
- if (imgUrl == null) {
- callback.onPostExecute(null);
- return;
- }
+ private AsyncLoadImageInterface onComplete = new AsyncLoadImageInterface() {
+ public GroundOverlayOptions options;
+ public PluginAsyncInterface callback;
+ public String imgUrl;
- AsyncLoadImage.AsyncLoadImageOptions imageOptions = new AsyncLoadImage.AsyncLoadImageOptions();
- imageOptions.height = -1;
- imageOptions.width = -1;
- imageOptions.noCaching = true;
- imageOptions.url = imgUrl;
- final AsyncLoadImage task = new AsyncLoadImage(cordova, webView, imageOptions, new AsyncLoadImageInterface() {
+ @Override
+ public void setParams(Object target, Object option, PluginAsyncInterface callback) {
+ this.imgUrl = (String) target;
+ this.options = (GroundOverlayOptions) option;
+ this.callback = callback;
+ }
+
@Override
public void onPostExecute(AsyncLoadImage.AsyncLoadImageResult result) {
+ imageLoadingTasks.remove(this.hashCode());
if (result == null || result.image == null) {
callback.onError("Can not read image from " + imgUrl);
return;
@@ -332,14 +336,30 @@ public class PluginGroundOverlay extends MyPlugin implements MyPluginInterface
callback.onPostExecute(groundOverlay);
}
- });
- cordova.getActivity().runOnUiThread(new Runnable() {
- @Override
- public void run() {
- task.execute();
+ };
+
+
+ private void setImage_(final GroundOverlayOptions options, final String imgUrl, final PluginAsyncInterface callback) {
+ if (imgUrl == null) {
+ callback.onPostExecute(null);
+ return;
}
- });
- imageLoadingTasks.add(task);
+
+ AsyncLoadImage.AsyncLoadImageOptions imageOptions = new AsyncLoadImage.AsyncLoadImageOptions();
+ imageOptions.height = -1;
+ imageOptions.width = -1;
+ imageOptions.noCaching = true;
+ imageOptions.url = imgUrl;
+
+ final AsyncLoadImage task = new AsyncLoadImage(cordova, webView, imageOptions, onComplete);
+ //cordova.getActivity().runOnUiThread(new Runnable() {
+ // @Override
+ // public void run() {
+ // task.execute();
+ // }
+ //});
+ task.execute();
+ imageLoadingTasks.put(task.hashCode(), task);
/*

MODIFY
src/android/plugin/google/maps/PluginMarker.java
src/android/plugin/google/maps/PluginMarker.java
package plugin.google.maps;
-import android.content.res.AssetManager;
-import android.content.res.Resources;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
@@ -28,15 +25,11 @@ import com.google.android.gms.maps.model.MarkerOptions;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
-import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.CordovaWebView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
@@ -49,9 +42,9 @@ public class PluginMarker extends MyPlugin implements MyPluginInterface {
BOUNCE
}
- protected ArrayList<AsyncTask> iconLoadingTasks = new ArrayList<AsyncTask>();
- protected ArrayList<Bitmap> icons = new ArrayList<Bitmap>();
- protected HashMap<String, Integer> iconCacheKeys = new HashMap<String, Integer>();
+ protected HashMap<Integer, AsyncTask> iconLoadingTasks = new HashMap<Integer, AsyncTask>();
+ protected HashMap<String, Bitmap> icons = new HashMap<String, Bitmap>();
+ protected final HashMap<String, Integer> iconCacheKeys = new HashMap<String, Integer>();
private static final Paint paint = new Paint();
private final HashMap<String, Integer> semaphoreAsync = new HashMap<String, Integer>();
@@ -131,10 +124,11 @@ public class PluginMarker extends MyPlugin implements MyPluginInterface {
cacheKeys = null;
}
if (icons != null) {
- Bitmap[] cachedBitmaps = icons.toArray(new Bitmap[icons.size()]);
+ String[] keys = icons.keySet().toArray(new String[icons.size()]);
+ //Bitmap[] cachedBitmaps = icons.toArray(new Bitmap[icons.size()]);
Bitmap image;
- for (int i = 0; i < cachedBitmaps.length; i++) {
- image = icons.remove(0);
+ for (int i = 0; i < keys.length; i++) {
+ image = icons.remove(keys[i]);
if (image != null && !image.isRecycled()) {
image.recycle();
}
@@ -985,15 +979,34 @@ public class PluginMarker extends MyPlugin implements MyPluginInterface {
height = sizeInfo.getInt("height", height);
}
+
final AsyncLoadImage.AsyncLoadImageOptions options = new AsyncLoadImage.AsyncLoadImageOptions();
options.url = iconUrl;
options.width = width;
options.height = height;
options.noCaching = noCaching;
- AsyncLoadImage task = new AsyncLoadImage(cordova, webView, options, new AsyncLoadImageInterface() {
+ onComplete.setParams(marker, iconProperty, callback);
+ AsyncLoadImage task = new AsyncLoadImage(cordova, webView, options, onComplete);
+ task.execute();
+ iconLoadingTasks.put(task.hashCode(), task);
+ }
+
+ private AsyncLoadImageInterface onComplete = new AsyncLoadImageInterface() {
+ public Marker marker;
+ public Bundle iconProperty;
+ public PluginAsyncInterface callback;
+
+ public void setParams(Object marker, Object iconProperty, PluginAsyncInterface callback) {
+ this.marker = (Marker) marker;
+ this.iconProperty = (Bundle) iconProperty;
+ this.callback = callback;
+ }
+
@Override
public void onPostExecute(AsyncLoadImage.AsyncLoadImageResult result) {
+ iconLoadingTasks.remove(this.hashCode());
+
if (result == null || result.image == null) {
callback.onPostExecute(marker);
return;
@@ -1004,7 +1017,12 @@ public class PluginMarker extends MyPlugin implements MyPluginInterface {
return;
}
- String currentCacheKey = (String) pluginMap.objects.get("marker_icon_" + marker.getTag());
+ String markerTag = marker.getTag() + "";
+ String markerIconTag = "marker_icon_" + markerTag;
+ String markerImgSizeTag = "marker_imageSize_" + markerTag;
+
+ String currentCacheKey = (String) pluginMap.objects.get(markerIconTag);
+ if (result.cacheKey != null && result.cacheKey.equals(currentCacheKey)) {
synchronized (iconCacheKeys) {
if (iconCacheKeys.containsKey(currentCacheKey)) {
int count = iconCacheKeys.get(currentCacheKey);
@@ -1017,8 +1035,16 @@ public class PluginMarker extends MyPlugin implements MyPluginInterface {
}
}
}
+ }
- icons.add(result.image);
+ if (icons.containsKey(markerIconTag)) {
+ Bitmap icon = icons.remove(markerIconTag);
+ if (icon != null && !icon.isRecycled()) {
+ icon.recycle();
+ }
+ icon = null;
+ }
+ icons.put(markerTag, result.image);
//-------------------------------------------------------
// Counts up the markers that use the same icon image.
@@ -1028,7 +1054,7 @@ public class PluginMarker extends MyPlugin implements MyPluginInterface {
callback.onPostExecute(marker);
return;
}
- String hitCountKey = "marker_icon_" + marker.getTag();
+ String hitCountKey = markerIconTag;
pluginMap.objects.put(hitCountKey, result.cacheKey);
if (!iconCacheKeys.containsKey(result.cacheKey)) {
iconCacheKeys.put(result.cacheKey, 1);
@@ -1036,6 +1062,7 @@ public class PluginMarker extends MyPlugin implements MyPluginInterface {
int count = iconCacheKeys.get(result.cacheKey);
iconCacheKeys.put(result.cacheKey, count + 1);
}
+ //Log.d(TAG, "----> " + result.cacheKey + " = " + iconCacheKeys.get(result.cacheKey));
}
//------------------------
@@ -1054,6 +1081,7 @@ public class PluginMarker extends MyPlugin implements MyPluginInterface {
// Sets image as icon
//------------------------
marker.setIcon(bitmapDescriptor);
+ bitmapDescriptor = null;
//---------------------------------------------
// Save the information for the anchor property
@@ -1061,7 +1089,11 @@ public class PluginMarker extends MyPlugin implements MyPluginInterface {
Bundle imageSize = new Bundle();
imageSize.putInt("width", result.image.getWidth());
imageSize.putInt("height", result.image.getHeight());
- self.pluginMap.objects.put("marker_imageSize_" + marker.getTag(), imageSize);
+ self.pluginMap.objects.remove(markerImgSizeTag);
+ self.pluginMap.objects.put(markerImgSizeTag, imageSize);
+
+ result.image.recycle();
+ result.image = null;
// The `anchor` of the `icon` property
if (iconProperty.containsKey("anchor")) {
@@ -1075,17 +1107,14 @@ public class PluginMarker extends MyPlugin implements MyPluginInterface {
// The `anchor` property for the infoWindow
if (iconProperty.containsKey("infoWindowAnchor")) {
double[] anchor = iconProperty.getDoubleArray("infoWindowAnchor");
- if (anchor.length == 2) {
+ if (anchor != null && anchor.length == 2) {
_setInfoWindowAnchor(marker, anchor[0], anchor[1], imageSize.getInt("width"), imageSize.getInt("height"));
}
}
callback.onPostExecute(marker);
}
- });
- task.execute();
- iconLoadingTasks.add(task);
- }
+ };
private void _setIconAnchor(final Marker marker, double anchorX, double anchorY, final int imageWidth, final int imageHeight) {
// The `anchor` of the `icon` property

Bug fix: memory leak on setIcon (on Android) #1677
Bug fix: setIcon leaking memory on Android #1649