Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MapboxTextureViewRenderer Memory Leak #2463

Closed
SigmaAppdevelopment opened this issue Aug 21, 2024 · 6 comments
Closed

MapboxTextureViewRenderer Memory Leak #2463

SigmaAppdevelopment opened this issue Aug 21, 2024 · 6 comments
Labels
bug 🪲 Something isn't working

Comments

@SigmaAppdevelopment
Copy link

SigmaAppdevelopment commented Aug 21, 2024

Environment

  • Android OS version: 14
  • Maps SDK Version: 11.6.0

Observed behavior and steps to reproduce

I want to show a list of routes with preview. therefore I'm using a mapview inside a recyclerview.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val binding = ItemRouteWithMapBinding.inflate(LayoutInflater.from(context), parent, false)
         val view = binding.root
         val viewHolder = RouteViewHolder(context, view, routesViewModel, true)
                
          viewHolder.onMapClick = {
              // do something
            }

           return viewHolder
}

If the fragment is stopped, I detach adapter from recycler view.

override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
        super.onViewDetachedFromWindow(holder)

        // Fragment stopped, adapter detached from recyclerview
        if (!attached) {
            if (holder is RouteViewHolder) {
                val mapView = holder.mapView
                mapView?.onDestroy()
            }
        }
    }
    

But there is stell a memory leak with MapboxTextureViewRenderer. If I set mapsurface to surfaceView, there is no leak

Expected behavior

No Leak

@SigmaAppdevelopment SigmaAppdevelopment added the bug 🪲 Something isn't working label Aug 21, 2024
@flasher297
Copy link
Contributor

Could you please clarify and share a snippet of what's inside RouteViewHolder and how you create the mapview.

@SigmaAppdevelopment
Copy link
Author

SigmaAppdevelopment commented Sep 9, 2024

Hi, sorry for the late answer, I was on holidays.

Here is our codesnipped RouteViewHolder:

var mapView: MapView? = null
private var mapboxMap: MapboxMap? = null
private var mapInitialized = false

private val onStyleLoadedListener = OnStyleLoadedListener {
    mapboxMap?.getStyle()?.let {
        onStyleLoaded(it)
    }
}

init {
    initMap()

    mapboxMap = mapView?.getMapboxMap()
    mapboxMap?.addOnStyleLoadedListener(onStyleLoadedListener)
}

private fun initMap() {
    mapView = MapView(context, mapInitOptions)
    val params = LinearLayout.LayoutParams(
        LinearLayout.LayoutParams.MATCH_PARENT,
        LinearLayout.LayoutParams.MATCH_PARENT,
        1.0f
    )
    binding.relativeLayout.addView(mapView, params)
}

private fun onStyleLoaded(it: Style) {
    it.localizeLabels(Locale.forLanguageTag(Locale.getDefault().toString()))

    initLineStringComponents(
        context,
        it,
        TRACK_SOURCE_ID,
        TRACK_LAYER_ID,
        MapPath.ROUTE
    )
    
    mapInitialized = true
}

fun initLineStringComponents(context: Context, style: Style, sourceId:String, layerId:String, mapPath: MapPath) {
    if (style.styleSources.find { it.id == sourceId } != null ) {
        Log.d(TAG, "lineString already exist")
        return
    }

    val initPoints = listOf(Point.fromLngLat(0.0, 0.0), Point.fromLngLat(0.0, 0.0))
    val source = geoJsonSource(sourceId) {
        feature(Feature.fromGeometry(LineString.fromLngLats(initPoints)))
    }
    style.addSource(source)
    
     val layer = lineLayer(layerId, sourceId) {
        lineCap(LineCap.ROUND)
        lineJoin(LineJoin.ROUND)
        lineWidth(pathWidth)
        lineColor(ContextCompat.getColor(context, colorId))
        visibility(Visibility.NONE)
    }

    addLayerBelowLabels(style, layer)
}

fun addLayerBelowLabels(style:Style, layer:Layer) {
    for (styleLayer in style.styleLayers) {
        if (styleLayer.id.indexOf("label") != -1) {
            try {
                val pos = LayerPosition(null, styleLayer.id, null)
                style.addPersistentLayer(layer, pos)
            } catch (exception: Exception) {
                FirebaseCrashlytics.getInstance().recordException(exception)
            }

            return
        }
    }
}

override fun bind(item: RouteItem) {
    super.bind(item)

    if (mapInitialized) {
        mapboxMap?.let { it1 ->
            MapUtils.setLayerVisibility(it1, TRACK_LAYER_ID, Visibility.NONE)
            MapUtils.updateLineStringComponent(it1, null, TRACK_SOURCE_ID, TRACK_LAYER_ID, Visibility.NONE)
        }
   }

   CoroutineScope(Dispatchers.IO).launch {
            routePoints = DataRepository.getInstance(context).loadRoutePointsSync(item.routeId)
            val lineString = LineString.fromLngLats(points)

            CoroutineScope(Dispatchers.Main).launch {
                if (points.isNotEmpty()) {
                    drawTrack(lineString)
                }
                updateCamera(points)
            }
        }
    }

@SigmaAppdevelopment
Copy link
Author

SigmaAppdevelopment commented Sep 9, 2024

Here mapInitOptions:

val mapOptions = MapOptions.Builder().applyDefaultParams(context)
            .constrainMode(ConstrainMode.WIDTH_AND_HEIGHT)
            .glyphsRasterizationOptions(
                GlyphsRasterizationOptions.Builder()
                    .rasterizationMode(GlyphsRasterizationMode.IDEOGRAPHS_RASTERIZED_LOCALLY)
                    .fontFamily("sans-serif")
                    .build()
            )
            .build()

        val plugins = listOf(
            Plugin.Mapbox(MAPBOX_LOGO_PLUGIN_ID),
            Plugin.Mapbox(MAPBOX_ATTRIBUTION_PLUGIN_ID)
        )

        val initialCameraOptions = CameraOptions.Builder()
            .center(Point.fromLngLat(-122.4194, 37.7749))
            .zoom(9.0)
            .bearing(120.0)
            .build()

        return MapInitOptions(
            context = context,
            mapOptions = mapOptions,
            plugins = plugins,
            cameraOptions = initialCameraOptions,
            textureView = true,
            styleUri = MapboxStyleManager.getListStyle(context.resources)
        )

@flasher297
Copy link
Contributor

@SigmaAppdevelopment thank you for the snippets.
I see that you call mapView?.onDestroy() only if the fragment is already not attached. Because of the asynchronous nature of Android and various cache pools of RV, there's a probability that some mapViews are not destroyed at all in this case.
You could check our example and try to call onStop() in onViewDetachedFromWindow

I tried to replicate your example but still can't reproduce the memory leak. It would be great if you could prepare a sample app that reproduces the issue.
It would allow us to see what holds the reference to MapboxTextureViewRenderer

@SigmaAppdevelopment
Copy link
Author

hi @flasher297 Calling onStop solved the issue, but there was a new problem: when scrolling up again map was gone. So we needed to initialize it again, and that was bad performance.

Because we needed a solution, I found another one. Snapshotter. The fragment holds one instance of Snapshotter and a queue of snapshot requests. The view holders sends a request to Snapshotter on bind and receives asynchronous callback when snapshot is ready. So recycler view shows a list of bitmaps, not a list of maps. Finally no leak, problem solved.

@flasher297
Copy link
Contributor

Nice to hear it and thank you for the feedback.
Feel free to raise a separate ticket if you face an issue with Snapshotter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🪲 Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants