1

I have Bottom Navigation View with 4 tab and one of them contains Huawei MapView. When I switch from Tab A which contains MapView to Tab B, Leak Canary show a memory leak which related to MapView

Fragment only contains MapView, there is not any extra view

class MapFragment : BaseFragment(R.layout.fragment_map), OnMapReadyCallback  {

    private var hMap: HuaweiMap? = null

    companion object {
        private const val MAPVIEW_BUNDLE_KEY = "MapViewBundleKey"
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        initHuaweiMap(savedInstanceState)
    }


    private fun initHuaweiMap(savedInstanceState: Bundle?) {
        var mapViewBundle: Bundle? = null
        if (savedInstanceState != null) {
            mapViewBundle = savedInstanceState.getBundle(MAPVIEW_BUNDLE_KEY)
        }
        map_view?.apply {
            onCreate(mapViewBundle)
            getMapAsync(this@MapFragment)
        }
    }

    override fun onMapReady(map: HuaweiMap?) {
        hMap = map
        hMap?.setMapStyle(MapStyleOptions.loadRawResourceStyle(requireContext(), R.raw.mapstyle_night_hms))
        hMap?.isMyLocationEnabled = false // Enable the my-location overlay.
        hMap?.uiSettings?.isMyLocationButtonEnabled = false // Enable the my-location icon.
        hMap?.uiSettings?.isZoomControlsEnabled = false // Disable zoom-in zoom-out buttons
    }


    override fun onStart() {
        map_view?.onStart()
        super.onStart()
    }

    override fun onStop() {
        map_view?.onStop()
        super.onStop()
    }

    override fun onDestroy() {
        map_view?.onDestroy()
        super.onDestroy()
    }

    override fun onPause() {
        map_view?.onPause()
        super.onPause()
    }

    override fun onResume() {
        map_view?.onResume()
        super.onResume()
    }

    override fun onLowMemory() {
        map_view?.onLowMemory()
        super.onLowMemory()
    }

    override fun onDestroyView() {
        hMap?.clear()
        hMap = null
        map_view?.onDestroy()
        super.onDestroyView()
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        var mapViewBundle = outState.getBundle(MAPVIEW_BUNDLE_KEY)
        if (mapViewBundle == null) {
            mapViewBundle = Bundle()
            outState.putBundle(MAPVIEW_BUNDLE_KEY, mapViewBundle)
        }
        map_view?.onSaveInstanceState(mapViewBundle)
    }
}

My Layout file

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".ui.tabs.profile.Mapragment">

    <com.huawei.hms.maps.MapView
        android:id="@+id/map_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:cameraZoom="14"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

I put all lifecycle method related to MapView but still cause a memory leak

Huawei MapView Memory Leak

Last step it says;

MapFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)

I clear the references at onDestroyView but still get same error. How can I prevent this memory leak?

ysfcyln
  • 2,857
  • 6
  • 34
  • 61

2 Answers2

0

You are advised to use MapFragment (extends the native Fragment component of Android and can be used to add a map to an app in the simplest way), or SupportMapFragment, instead of using your Fragment to embed Huawei MapView.


Update: Please check the following codes:

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {//1
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        LeakCanary.install(this);
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
    }
}
package com.huawei.googlemaptankillereproduce

import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction

internal class FragmentController(fragmentManager: FragmentManager, containerId: Int, savedInstanceState: Bundle?) {
    companion object {
        private const val TAG = "FragmentController"

        private const val TAB_COUNT = 3
        private const val TAG_FRAGMENT_1 = "Fragment1"
        private const val TAG_FRAGMENT_2 = "Fragment2"
        private const val TAG_FRAGMENT_3 = "Fragment3"
        private const val STATE_ACTIVE_TAB = "StateActiveTab"

        const val TAB_1 = 0
        const val TAB_2 = 1
        const val TAB_3 = 2
    }

    private val mFragmentManager = fragmentManager
    private val mContainerId = containerId
    private var mSelectedTab = -1

    init {
        if (savedInstanceState != null) {
            mSelectedTab = savedInstanceState.getInt(STATE_ACTIVE_TAB)
        }
        else {
            activateTab(TAB_2)
        }
    }

    fun onSaveInstanceState(outState: Bundle) {
        outState.putInt(STATE_ACTIVE_TAB, mSelectedTab)
    }

    fun activateTab(tab: Int) {
        if (tab != mSelectedTab) {
            val fragmentTransaction = mFragmentManager.beginTransaction()

            if (mSelectedTab != -1) {
                hideFragment(mSelectedTab, fragmentTransaction)
            }

            mSelectedTab = tab
            showFragment(tab, fragmentTransaction)

            fragmentTransaction.commitAllowingStateLoss()
        }
    }

    private fun hideFragment(view: Int, ft: FragmentTransaction) {
        val fragment = getExistingFragment(view)
        if (fragment != null) {
            ft.detach(fragment)
        }
    }

    private fun showFragment(tab: Int, ft: FragmentTransaction) {
        val fragment = getExistingFragment(tab)
        if (fragment != null) {
            ft.attach(fragment)
        }
        else {
            ft.add(mContainerId, getNewFragment(tab)!!, getTag(tab))
        }
    }

    private fun getExistingFragment(view: Int): Fragment? {
        return mFragmentManager.findFragmentByTag(getTag(view))
    }

    private fun getNewFragment(tab: Int): Fragment? {
        return when (tab) {
            TAB_1 -> MapFragment()
            TAB_2 -> MapFragment()
            TAB_3 -> MapFragment()
            else -> null
        }
    }

    private fun getTag(tab: Int): String {
        return when (tab) {
            TAB_1 -> TAG_FRAGMENT_1
            TAB_2 -> TAG_FRAGMENT_2
            TAB_3 -> TAG_FRAGMENT_3
            else -> ""
        }
    }
}
package com.huawei.googlemaptankillereproduce

import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.bottomnavigation.BottomNavigationView

class MainActivity : AppCompatActivity() {

    private lateinit var mFragmentController: FragmentController

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mFragmentController = FragmentController(supportFragmentManager, R.id.fragment_container, savedInstanceState)

        val bottomBar = findViewById<BottomNavigationView>(R.id.bottom_navigation)
        bottomBar.selectedItemId = R.id.bottom_bar_2
        bottomBar.setOnNavigationItemSelectedListener { item: MenuItem ->
            when (item.itemId) {
                R.id.bottom_bar_1 -> mFragmentController.activateTab(FragmentController.TAB_1)
                R.id.bottom_bar_2 -> mFragmentController.activateTab(FragmentController.TAB_2)
                R.id.bottom_bar_3 -> mFragmentController.activateTab(FragmentController.TAB_3)
            }
            true
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mFragmentController.onSaveInstanceState(outState)
    }

}
package com.huawei.googlemaptankillereproduce

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.huawei.hms.maps.HuaweiMap
import com.huawei.hms.maps.SupportMapFragment

class MapFragment : Fragment(),  com.huawei.hms.maps.OnMapReadyCallback {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_map, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment?
        mapFragment?.getMapAsync(this)

    }


    override fun onMapReady(p0: HuaweiMap?) {
//        TODO("Not yet implemented")
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:contentInsetLeft="0dp"
            app:contentInsetStart="0dp" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="0dp"
        android:paddingTop="0dp"
        android:paddingRight="0dp"
        android:paddingBottom="56dp"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="56dp"
        android:layout_gravity="bottom"
        android:background="@android:color/white"
        app:menu="@menu/bottombar_menu" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

fragment_map.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView

        android:id="@+id/map"
        android:name="com.huawei.hms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>
dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'com.google.android.gms:play-services-maps:17.0.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    implementation "androidx.fragment:fragment:1.2.5"

    implementation 'com.huawei.hms:maps:5.0.5.301'

    implementation 'com.squareup.leakcanary:leakcanary-android:1.5'
}
zhangxaochen
  • 32,744
  • 15
  • 77
  • 108
  • Replace ```MapView``` with ```SupportMapFragment``` but still this leak exists. is it possible to be related SDK? Because it says ```com.huawei.hms.maps.SupportMapFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks) and View detached and has parent``` – ysfcyln Dec 04 '20 at 07:59
  • @ysfcyln Please try [Map Kit 5.0.5.301](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/hmssdk-kit-0000001050042513#EN-US_TOPIC_0000001050042513__section219714610910) first to see whether could fix it. – zhangxaochen Dec 04 '20 at 08:46
  • maybe leak is related about ```onMapReady``` callback because every time when I change tab ```onMapReady``` callback triggered multiple times. For example first run ```onMapReady``` called once it is okey. Then change tab and reopen map tab, this time ```onMapReady``` called two times and further more if I change tab and reopen map tab again this time called three times. Old listener references still exists – ysfcyln Dec 04 '20 at 14:04
  • You may add cache to fragment. You enter fragment and load the map only in the first time, and the rest do not need to load it and just use the cache. Please check the code I updated again. :) @ysfcyln – zhangxaochen Dec 07 '20 at 02:16
  • Thanks @shirley Holding a global instance of support map fragment and making null check before ```getMapAsync()``` solve listener stack error but leak stil exists :'( – ysfcyln Dec 09 '20 at 07:07
  • Please check the toggle controls and fragment. I added mine in the answer. @ysfcyln – zhangxaochen Dec 09 '20 at 09:04
0

The issue may be that the map is continuing to check location after being destroyed. Try inserting hMap?.setMyLocationEnabled(false); into onDestroy() to force it to no longer track location after being destroyed.

Zinna
  • 1,947
  • 2
  • 5
  • 20