googlemaps / android-maps-compose

Jetpack Compose composables for the Maps SDK for Android
https://developers.google.com/maps/documentation/android-sdk/maps-compose
Apache License 2.0
1.09k stars 128 forks source link

OutOfMemory exception when showing/hiding GoogleMaps composable #581

Open dudeck opened 3 weeks ago

dudeck commented 3 weeks ago

Environment details

  1. Specify the API at the beginning of the title (for example, "Places: ...") Google Maps Compose Android

  2. OS type and version Mac OS Sonoma 14.5 Android API 29 emulator

  3. Library version and other environment information gms-maps-compose = "com.google.maps.android:maps-compose:5.0.3" gms-maps-compose-utils = "com.google.maps.android:maps-compose-utils:5.0.3"

Steps to reproduce

  1. Add yours Google Maps API Key.
  2. Create Android Emulator like Pixel 3, API 29 arm64 with GooglePlay.
  3. Build app from attached code.
  4. Click multiple times on Show/Hide Map button.
  5. After at least 15 tries application crashes with OutOfMemory Exception
  6. See how Memory consumption is rising after each time map is shown.

Code example

// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.example.GMC

import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.StrokeStyle
import com.google.android.gms.maps.model.StyleSpan
import com.google.maps.android.compose.CameraPositionState
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.MapProperties
import com.google.maps.android.compose.MapType
import com.google.maps.android.compose.MapUiSettings
import com.google.maps.android.compose.MarkerInfoWindowContent
import com.google.maps.android.compose.rememberCameraPositionState
import com.google.maps.android.compose.rememberMarkerState

private const val TAG = "ScrollingMapActivity"

val singapore = LatLng(1.3588227, 103.8742114)
val singapore2 = LatLng(1.40, 103.77)
val singapore3 = LatLng(1.45, 103.77)
val singapore4 = LatLng(1.50, 103.77)
val singapore5 = LatLng(1.3418, 103.8461)
val singapore6 = LatLng(1.3430, 103.8844)
val singapore7 = LatLng(1.3430, 103.9116)
val singapore8 = LatLng(1.3300, 103.8624)
val singapore9 = LatLng(1.3200, 103.8541)
val singapore10 = LatLng(1.3200, 103.8765)

val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, 11f)

val styleSpan = StyleSpan(
    StrokeStyle.gradientBuilder(
        Color.Red.toArgb(),
        Color.Green.toArgb(),
    ).build(),
)

class MapInColumnActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // Observing and controlling the camera's state can be done with a CameraPositionState
            val cameraPositionState = rememberCameraPositionState {
                position = defaultCameraPosition
            }
            var columnScrollingEnabled by remember { mutableStateOf(true) }

            // Use a LaunchedEffect keyed on the camera moving state to enable column scrolling when the camera stops moving
            LaunchedEffect(cameraPositionState.isMoving) {
                if (!cameraPositionState.isMoving) {
                    columnScrollingEnabled = true
                    Log.d(TAG, "Map camera stopped moving - Enabling column scrolling...")
                }
            }

            MapInColumn(
                modifier = Modifier.fillMaxSize(),
                cameraPositionState,
                columnScrollingEnabled = columnScrollingEnabled,
                onMapTouched = {
                    columnScrollingEnabled = false
                    Log.d(
                        TAG,
                        "User touched map - Disabling column scrolling after user touched this Box..."
                    )
                },
                onMapLoaded = { }
            )
        }
    }
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun MapInColumn(
    modifier: Modifier = Modifier,
    cameraPositionState: CameraPositionState,
    columnScrollingEnabled: Boolean,
    onMapTouched: () -> Unit,
    onMapLoaded: () -> Unit,
) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colors.background
    ) {
        var isMapLoaded by remember { mutableStateOf(false) }
        var showMap by remember { mutableStateOf(true) }

        Column(
            Modifier
                .fillMaxSize()
                .verticalScroll(
                    rememberScrollState(),
                    columnScrollingEnabled
                ),
            horizontalAlignment = Alignment.Start
        ) {
            Spacer(modifier = Modifier.padding(10.dp))
            Button(onClick = { showMap = !showMap }) {
                Text(text = "Show/Hide map")
            }
            for (i in 1..20) {
                Text(
                    text = "Item $i",
                    modifier = Modifier
                        .padding(start = 10.dp)
                        .testTag("Item $i")
                )
            }
            Spacer(modifier = Modifier.padding(10.dp))

            Box(
                Modifier
                    .fillMaxWidth()
                    .height(200.dp)
            ) {
                if (showMap) GoogleMapViewInColumn(
                    modifier = Modifier
                        .fillMaxSize()
                        .testTag("Map")
                        .pointerInteropFilter(
                            onTouchEvent = {
                                when (it.action) {
                                    MotionEvent.ACTION_DOWN -> {
                                        onMapTouched()
                                        false
                                    }

                                    else -> {
                                        Log.d(
                                            TAG,
                                            "MotionEvent ${it.action} - this never triggers."
                                        )
                                        true
                                    }
                                }
                            }
                        ),
                    cameraPositionState = cameraPositionState,
                    onMapLoaded = {
                        isMapLoaded = true
                        onMapLoaded()
                    },
                )
                if (!isMapLoaded) {
                    androidx.compose.animation.AnimatedVisibility(
                        modifier = Modifier
                            .fillMaxSize(),
                        visible = !isMapLoaded,
                        enter = EnterTransition.None,
                        exit = fadeOut()
                    ) {
                        CircularProgressIndicator(
                            modifier = Modifier
                                .background(MaterialTheme.colors.background)
                                .wrapContentSize()
                        )
                    }
                }
            }
            Spacer(modifier = Modifier.padding(10.dp))
            for (i in 21..40) {
                Text(
                    text = "Item $i",
                    modifier = Modifier
                        .padding(start = 10.dp)
                        .testTag("Item $i")
                )
            }
            Spacer(modifier = Modifier.padding(10.dp))
        }
    }
}

@Composable
private fun GoogleMapViewInColumn(
    modifier: Modifier,
    cameraPositionState: CameraPositionState,
    onMapLoaded: () -> Unit,
) {
    val singaporeState = rememberMarkerState(position = singapore)

    var uiSettings by remember { mutableStateOf(MapUiSettings(compassEnabled = false)) }
    var mapProperties by remember {
        mutableStateOf(MapProperties(mapType = MapType.NORMAL))
    }

    GoogleMap(
        modifier = modifier,
        cameraPositionState = cameraPositionState,
        properties = mapProperties,
        uiSettings = uiSettings,
        onMapLoaded = onMapLoaded
    ) {
        // Drawing on the map is accomplished with a child-based API
        val markerClick: (Marker) -> Boolean = {
            Log.d(TAG, "${it.title} was clicked")
            cameraPositionState.projection?.let { projection ->
                Log.d(TAG, "The current projection is: $projection")
            }
            false
        }
        MarkerInfoWindowContent(
            state = singaporeState,
            title = "Singapore",
            onClick = markerClick,
            draggable = true,
        ) {
            Text(it.title ?: "Title", color = Color.Red)
        }
    }
}

Stack trace

java.lang.OutOfMemoryError: Failed to allocate a 118720 byte allocation with 106056 free bytes and 103KB until OOM, target footprint 50331648, growth limit 50331648
                                                                                                        at m.fdu.d(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:22)
                                                                                                        at m.fea.c(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:13)
                                                                                                        at m.enc.k(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:8)
                                                                                                        at m.etl.a(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:13)
                                                                                                        at m.eum.d(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:116)
                                                                                                        at m.erb.run(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:164)
                                                                                                        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
                                                                                                        at m.cab.run(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:12)
                                                                                                        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
                                                                                                        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
                                                                                                        at m.car.run(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:40)

Note: I checked in Memory Profiler that memory increases from around 350 MB to 450MB and then crashes. It is easy to reproduce, need some older/low end device (tested on emulators). It blocks us of releasing feature to the client. I used your sample code from GoogleMaps repo:https://github.com/googlemaps/android-maps-compose just modifying by adding Button to change visibility state of Google maps in column.

Could you fix it, please? Or at least give us some temporary quick fix solution? Thank you in advance.

dkhawk commented 3 weeks ago

Can be triggered with this minimal example

class MinimumMapActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            var showMap by remember { mutableStateOf(true) }

            val cameraPositionState: CameraPositionState = rememberCameraPositionState() {
                position = defaultCameraPosition
            }

            val uiSettings by remember { mutableStateOf(MapUiSettings(compassEnabled = false)) }

            val mapProperties by remember {
                mutableStateOf(MapProperties(mapType = MapType.NORMAL))
            }

            LaunchedEffect(Unit) {
                while (true) {
                    delay(100.milliseconds.toJavaDuration())
                    showMap = !showMap
                }
            }

            if (showMap) {
                GoogleMap(
                    modifier = Modifier.fillMaxSize(),
                    cameraPositionState = cameraPositionState,
                    properties = mapProperties,
                    uiSettings = uiSettings,
                )
            }
        }
    }
}

Stack trace:

Process: com.example.memorybug, PID: 10996 java.lang.OutOfMemoryError: Failed to allocate a 475987 byte allocation with 245072 free bytes and 239KB until OOM, target footprint 50331648, growth limit 50331648 at dalvik.system.VMRuntime.newNonMovableArray(Native Method) at java.nio.DirectByteBuffer$MemoryRef.(DirectByteBuffer.java:70) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:258) at m.fab.Y(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:8) at m.fab.B(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:1) at m.fdf.a(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:12) at m.fdj.a(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:29) at m.ezb.x(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:12) at m.fbq.a(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:25) at m.ezm.e(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:170) at m.ezw.run(:com.google.android.gms.policy_maps_core_dynamite@241610205@241610202042.636179997.636179997:2346)

kikoso commented 2 weeks ago

I am unfortunately not able to reproduce this on the emulator, @dkhawk . Are you using a real device?

dkhawk commented 2 weeks ago

This was on a pixel 6 (IIRC).