Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

[core][android] Provide API to control eviction of cached images #14610

Merged
merged 6 commits into from
May 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions include/mbgl/map/map_observer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class MapObserver {
virtual void onSourceChanged(style::Source&) {}
virtual void onDidBecomeIdle() {}
virtual void onStyleImageMissing(const std::string&) {}
// This method should return true if unused image can be removed,
// false otherwise. By default, unused image will be removed.
virtual bool onCanRemoveUnusedStyleImage(const std::string&) { return true; }
};

} // namespace mbgl
1 change: 1 addition & 0 deletions include/mbgl/renderer/renderer_observer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class RendererObserver {
// Style is missing an image
using StyleImageMissingCallback = std::function<void()>;
virtual void onStyleImageMissing(const std::string&, StyleImageMissingCallback done) { done(); }
virtual void onRemoveUnusedStyleImages(const std::vector<std::string>&) {}
};

} // namespace mbgl
4 changes: 4 additions & 0 deletions include/mbgl/util/constants.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ constexpr uint8_t DEFAULT_PREFETCH_ZOOM_DELTA = 4;

constexpr uint64_t DEFAULT_MAX_CACHE_SIZE = 50 * 1024 * 1024;

// Default ImageManager's cache size for images added via onStyleImageMissing API.
// Average sprite size with 1.0 pixel ratio is ~2kB, 8kB for pixel ratio of 2.0.
constexpr std::size_t DEFAULT_ON_DEMAND_IMAGES_CACHE_SIZE = 100 * 8192;

constexpr Duration DEFAULT_TRANSITION_DURATION = Milliseconds(300);
constexpr Seconds CLOCK_SKEW_RETRY_TIMEOUT { 30 };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class MapChangeReceiver implements NativeMapView.StateCallback {
private final List<MapView.OnSourceChangedListener> onSourceChangedListenerList = new CopyOnWriteArrayList<>();
private final List<MapView.OnStyleImageMissingListener> onStyleImageMissingListenerList
= new CopyOnWriteArrayList<>();
private final List<MapView.OnCanRemoveUnusedStyleImageListener> onCanRemoveUnusedStyleImageListenerList
= new CopyOnWriteArrayList<>();

@Override
public void onCameraWillChange(boolean animated) {
Expand Down Expand Up @@ -230,6 +232,29 @@ public void onStyleImageMissing(String imageId) {
}
}

@Override
public boolean onCanRemoveUnusedStyleImage(String imageId) {
if (onCanRemoveUnusedStyleImageListenerList.isEmpty()) {
return true;
}

try {
if (!onCanRemoveUnusedStyleImageListenerList.isEmpty()) {
boolean canRemove = true;
for (MapView.OnCanRemoveUnusedStyleImageListener listener : onCanRemoveUnusedStyleImageListenerList) {
canRemove &= listener.onCanRemoveUnusedStyleImage(imageId);
}

return canRemove;
}
} catch (Throwable err) {
Logger.e(TAG, "Exception in onCanRemoveUnusedStyleImage", err);
throw err;
}

return true;
}

void addOnCameraWillChangeListener(MapView.OnCameraWillChangeListener listener) {
onCameraWillChangeListenerList.add(listener);
}
Expand Down Expand Up @@ -342,6 +367,14 @@ void removeOnStyleImageMissingListener(MapView.OnStyleImageMissingListener liste
onStyleImageMissingListenerList.remove(listener);
}

void addOnCanRemoveUnusedStyleImageListener(MapView.OnCanRemoveUnusedStyleImageListener listener) {
onCanRemoveUnusedStyleImageListenerList.add(listener);
}

void removeOnCanRemoveUnusedStyleImageListener(MapView.OnCanRemoveUnusedStyleImageListener listener) {
onCanRemoveUnusedStyleImageListenerList.remove(listener);
}

void clear() {
onCameraWillChangeListenerList.clear();
onCameraIsChangingListenerList.clear();
Expand All @@ -357,5 +390,6 @@ void clear() {
onDidFinishLoadingStyleListenerList.clear();
onSourceChangedListenerList.clear();
onStyleImageMissingListenerList.clear();
onCanRemoveUnusedStyleImageListenerList.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,33 @@ public void removeOnStyleImageMissingListener(@NonNull OnStyleImageMissingListen
mapChangeReceiver.removeOnStyleImageMissingListener(listener);
}

/**
* Set a callback that's invoked when map needs to release unused image resources.
alexshalamov marked this conversation as resolved.
Show resolved Hide resolved
*
* A callback will be called only for unused images that were provided by the client via
* {@link OnStyleImageMissingListener#onStyleImageMissing(String)} listener interface.
*
* By default, platform will remove unused images from the style. By adding listener, default
* behavior can be overridden and client can control whether to release unused resources.
*
* @param listener The callback that's invoked when map needs to release unused image resources
*/
public void addOnCanRemoveUnusedStyleImageListener(@NonNull OnCanRemoveUnusedStyleImageListener listener) {
mapChangeReceiver.addOnCanRemoveUnusedStyleImageListener(listener);
}

/**
* Removes a callback that's invoked when map needs to release unused image resources.
*
* When all listeners are removed, platform will fallback to default behavior, which is to remove
* unused images from the style.
*
* @param listener The callback that's invoked when map needs to release unused image resources
*/
public void removeOnCanRemoveUnusedStyleImageListener(@NonNull OnCanRemoveUnusedStyleImageListener listener) {
mapChangeReceiver.removeOnCanRemoveUnusedStyleImageListener(listener);
}

/**
* Interface definition for a callback to be invoked when the camera will change.
* <p>
Expand Down Expand Up @@ -994,6 +1021,22 @@ public interface OnStyleImageMissingListener {
void onStyleImageMissing(@NonNull String id);
}

/**
* Interface definition for a callback to be invoked with an unused image identifier.
* <p>
* {@link MapView#addOnCanRemoveUnusedStyleImageListener(OnCanRemoveUnusedStyleImageListener)}
* </p>
*/
public interface OnCanRemoveUnusedStyleImageListener {
/**
* Called when the map needs to release unused image resources.
*
* @param id of an image that is not used by the map and can be removed from the style.
* @return true if image can be removed, false otherwise.
*/
boolean onCanRemoveUnusedStyleImage(@NonNull String id);
}

/**
* Sets a callback object which will be triggered when the {@link MapboxMap} instance is ready to be used.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,15 @@ private void onStyleImageMissing(String imageId) {
}
}

@Keep
private boolean onCanRemoveUnusedStyleImage(String imageId) {
if (stateCallback != null) {
return stateCallback.onCanRemoveUnusedStyleImage(imageId);
}

return true;
}

@Keep
protected void onSnapshotReady(@Nullable Bitmap mapContent) {
if (checkState("OnSnapshotReady")) {
Expand Down Expand Up @@ -1463,5 +1472,7 @@ interface StateCallback extends StyleCallback {
void onSourceChanged(String sourceId);

void onStyleImageMissing(String imageId);

boolean onCanRemoveUnusedStyleImage(String imageId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package com.mapbox.mapboxsdk.testapp.maps

import android.graphics.Bitmap
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.MapView
import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.testapp.R
import com.mapbox.mapboxsdk.testapp.activity.espresso.EspressoTestActivity
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

@RunWith(AndroidJUnit4::class)
class RemoveUnusedImagesTest {

@Rule
@JvmField
var rule = ActivityTestRule(EspressoTestActivity::class.java)

private lateinit var mapView: MapView
private lateinit var mapboxMap: MapboxMap
private val latch = CountDownLatch(1)

@Before
fun setup() {
rule.runOnUiThread {
mapView = rule.activity.findViewById(R.id.mapView)
mapView.getMapAsync {
mapboxMap = it;
mapboxMap.setStyle(Style.Builder().fromJson(styleJson))
}
}
}

@Test
fun testRemoveUnusedImagesUserProvidedListener() {
var callbackLatch = CountDownLatch(2)
rule.runOnUiThread {
mapView.addOnStyleImageMissingListener {
mapboxMap.style!!.addImage(it, Bitmap.createBitmap(512, 512, Bitmap.Config.ARGB_8888))
}

// Remove layer and source, so that rendered tiles are no longer used, therefore, map must
// notify client about unused images.
mapView.addOnDidBecomeIdleListener {
mapboxMap.style!!.removeLayer("icon")
mapboxMap.style!!.removeSource("geojson")
}

mapView.addOnCanRemoveUnusedStyleImageListener {
callbackLatch.countDown()
mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(0.0, 120.0), 8.0))
mapView.addOnDidFinishRenderingFrameListener {
assertNotNull(mapboxMap.style!!.getImage("small"))
assertNotNull(mapboxMap.style!!.getImage("large"))
latch.countDown()
}
return@addOnCanRemoveUnusedStyleImageListener false
}
}

if(!latch.await(5, TimeUnit.SECONDS) && !callbackLatch.await(5, TimeUnit.SECONDS)){
throw TimeoutException()
}
}

@Test
fun testRemoveUnusedImagesDefaultListener() {
rule.runOnUiThread {
mapView.addOnStyleImageMissingListener {
mapboxMap.style!!.addImage(it, Bitmap.createBitmap(512, 512, Bitmap.Config.ARGB_8888))
}

// Remove layer and source, so that rendered tiles are no longer used, thus
// map must request removal of unused images.
mapView.addOnDidBecomeIdleListener {
mapboxMap.style!!.removeLayer("icon")
mapboxMap.style!!.removeSource("geojson")
mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(0.0, 120.0), 8.0))

// Wait for the next frame and check that images were removed from the style.
mapView.addOnDidFinishRenderingFrameListener {
if (mapboxMap.style!!.getImage("small") == null && mapboxMap.style!!.getImage("large") == null) {
latch.countDown()
}
}
}
}

if(!latch.await(5, TimeUnit.SECONDS)){
throw TimeoutException()
}
}

companion object {
private const val styleJson = """
{
"version": 8,
"name": "Mapbox Streets",
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"image": "small"
},
"geometry": {
"type": "Point",
"coordinates": [
0,
0
]
}
},
{
"type": "Feature",
"properties": {
"image": "large"
},
"geometry": {
"type": "Point",
"coordinates": [
1,
1
]
}
}
]
}
}
},
"layers": [{
"id": "bg",
"type": "background",
"paint": {
"background-color": "#f00"
}
},{
"id": "icon",
"type": "symbol",
"source": "geojson",
"layout": {
"icon-image": ["get", "image"]
}
}]
}
"""
}
}
4 changes: 4 additions & 0 deletions platform/android/src/android_renderer_frontend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class ForwardingRendererObserver : public RendererObserver {
delegate.invoke(&RendererObserver::onStyleImageMissing, id, done);
}

void onRemoveUnusedStyleImages(const std::vector<std::string>& ids) override {
delegate.invoke(&RendererObserver::onRemoveUnusedStyleImages, ids);
}

private:
std::shared_ptr<Mailbox> mailbox;
ActorRef<RendererObserver> delegate;
Expand Down
14 changes: 14 additions & 0 deletions platform/android/src/native_map_view.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,20 @@ void NativeMapView::onStyleImageMissing(const std::string& imageId) {
}
}

bool NativeMapView::onCanRemoveUnusedStyleImage(const std::string& imageId) {
assert(vm != nullptr);
alexshalamov marked this conversation as resolved.
Show resolved Hide resolved

android::UniqueEnv _env = android::AttachEnv();
static auto& javaClass = jni::Class<NativeMapView>::Singleton(*_env);
static auto onCanRemoveUnusedStyleImage = javaClass.GetMethod<jboolean (jni::String)>(*_env, "onCanRemoveUnusedStyleImage");
auto weakReference = javaPeer.get(*_env);
if (weakReference) {
return weakReference.Call(*_env, onCanRemoveUnusedStyleImage, jni::Make<jni::String>(*_env, imageId));
}

return true;
}

// JNI Methods //

void NativeMapView::resizeView(jni::JNIEnv&, int w, int h) {
Expand Down
1 change: 1 addition & 0 deletions platform/android/src/native_map_view.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class NativeMapView : public MapObserver {
void onDidFinishLoadingStyle() override;
void onSourceChanged(mbgl::style::Source&) override;
void onStyleImageMissing(const std::string&) override;
bool onCanRemoveUnusedStyleImage(const std::string&) override;

// JNI //

Expand Down
8 changes: 8 additions & 0 deletions src/mbgl/map/map_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,12 @@ void Map::Impl::onStyleImageMissing(const std::string& id, std::function<void()>
onUpdate();
}

void Map::Impl::onRemoveUnusedStyleImages(const std::vector<std::string>& unusedImageIDs) {
for (const auto& unusedImageID : unusedImageIDs) {
if (observer.onCanRemoveUnusedStyleImage(unusedImageID)) {
style->removeImage(unusedImageID);
}
}
}

} // namespace mbgl
1 change: 1 addition & 0 deletions src/mbgl/map/map_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Map::Impl : public style::Observer, public RendererObserver {
void onWillStartRenderingMap() final;
void onDidFinishRenderingMap() final;
void onStyleImageMissing(const std::string&, std::function<void()>) final;
void onRemoveUnusedStyleImages(const std::vector<std::string>&) final;

// Map
void jumpTo(const CameraOptions&);
Expand Down
Loading