BottomSheetDialogFragment показывает утечку памяти в LeakCanary 2, но я не уверен, почему? - PullRequest
0 голосов
/ 20 февраля 2020

Я просмотрел несколько сообщений и не нашел ничего, что помогло бы мне понять, почему LeakCanary сообщает об утечке. У меня есть основной вид деятельности с com.google.android.material.bottomappbar.BottomAppBar, и это показывает BottomSheetDialogFragment. Когда вы выбираете элемент на нижнем листе, он обновляет некоторый текст, а затем закрывает фрагмент диалога. Теперь выполнение этого с LeakCanary показывает утечку, когда диалоговое окно представлено и элемент выбран.

Утечка выглядит следующим образом:

    ====================================
    HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS

    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.

    152090 bytes retained by leaking objects
    Signature: 3841703253a9bf9893936b1dd318c9dd54bf5a8
    ┬───
    │ GC Root: System class
    │
    ├─ android.app.ActivityThread class
    │    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
    │    ↓ static ActivityThread.sCurrentActivityThread
    ├─ android.app.ActivityThread instance
    │    Leaking: NO (MainActivity↓ is not leaking)
    │    ↓ ActivityThread.mActivities
    ├─ android.util.ArrayMap instance
    │    Leaking: NO (MainActivity↓ is not leaking)
    │    ↓ ArrayMap.mArray
    ├─ java.lang.Object[] array
    │    Leaking: NO (MainActivity↓ is not leaking)
    │    ↓ Object[].[1]
    ├─ android.app.ActivityThread$ActivityClientRecord instance
    │    Leaking: NO (MainActivity↓ is not leaking)
    │    ↓ ActivityThread$ActivityClientRecord.activity
    ├─ com.example.testleak.MainActivity instance
    │    Leaking: NO (Activity#mDestroyed is false)
    │    ↓ MainActivity.bottomNavFragment
    │                   ~~~~~~~~~~~~~~~~~
    ╰→ com.example.testleak.BottomNavigationDrawerFragment instance
    ​     Leaking: YES (ObjectWatcher was watching this because com.example.testleak.BottomNavigationDrawerFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
    ​     key = 74db5e32-a816-418a-9f83-2f50a05f37a4
    ​     watchDurationMillis = 7224
    ​     retainedDurationMillis = 2210
    ​     key = 92744a08-2122-4fbe-9841-08f805fcf6e5
    ​     retainedDurationMillis = 2212
    ====================================
    0 LIBRARY LEAKS

    Library Leaks are leaks coming from the Android Framework or Google libraries.
    ====================================
    METADATA

    Please include this in bug reports and Stack Overflow questions.

    Build.VERSION.SDK_INT: 26
    Build.MANUFACTURER: motorola
    LeakCanary version: 2.1
    App process name: com.example.testleak
    Analysis duration: 4182 ms
    Heap dump file path: /data/user/0/com.example.testleak/files/leakcanary/2020-02-20_10-42-59_515.hprof
    Heap dump timestamp: 1582213384806
    ====================================

Таким образом, похоже, что BottomNavigationDrawerFragment следует очистить что-то в этом onDestroy() методе.

Вот основные вовлеченные файлы.

MainActivity.kt

package com.example.testleak

import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import com.example.testleak.BottomNavigationDrawerFragment.OnItemClickListener
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.content_main.*

class MainActivity : AppCompatActivity() {
    private var bottomNavFragment: BottomNavigationDrawerFragment? = null

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

        setSupportActionBar(bottom_app_bar)
        bottomNavFragment = BottomNavigationDrawerFragment(clickListener)
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        return true
    }

    private var clickListener = object : OnItemClickListener {
        override fun onItemClick(item: Int?) {
            // Based on the item clicked show that fragment
            bottomNavFragment?.dismiss()
            when (item) {
                R.id.nav_item_1 -> {
                    screen_label.text = resources.getString(R.string.item_1)
                }
                R.id.nav_item_2 -> {
                    screen_label.text = resources.getString(R.string.item_2)
                }
                R.id.nav_item_3 -> {
                    screen_label.text = resources.getString(R.string.item_3)
                }
                R.id.nav_item_4 -> {
                    screen_label.text = resources.getString(R.string.item_4)
                }
                R.id.preferences -> {
                    screen_label.text = resources.getString(R.string.preferences)
                }
            }
        }
    }


    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        when (item!!.itemId) {
            android.R.id.home -> {
                bottomNavFragment!!.show(supportFragmentManager, bottomNavFragment!!.tag)
            }
        }
        return true
    }
}

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/coordinatorLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include layout="@layout/content_main" />

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottom_app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:backgroundTint="?attr/colorPrimary"
        app:fabAlignmentMode="center"
        app:hideOnScroll="true"
        app:layout_scrollFlags="scroll|enterAlways"
        app:navigationIcon="@drawable/ic_menu_white_24dp"/>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        style="@style/Widget.MaterialComponents.FloatingActionButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_add_circle_outline_white_24dp"
        app:layout_anchor="@id/bottom_app_bar" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

content_main. xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/coordinatorLayout2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/constraintLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/screen_label"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="120dp"
                android:text="Primary"
                android:textSize="18sp"
                android:textStyle="bold"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

</LinearLayout>

BottomNavigationDrawerFragment.kt

package com.example.testleak

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.navigation.NavigationView

class BottomNavigationDrawerFragment(private val clickListener: OnItemClickListener) : BottomSheetDialogFragment() {

    lateinit var mapNavView: NavigationView

    interface OnItemClickListener {
        fun onItemClick(item: Int?)
    }

    override fun onDestroy() {
        Log.d("BottomNavFragment", "onDestroy")
        super.onDestroy()
    }

    override fun onDestroyView() {
        Log.d("BottomNavFragment", "onDestroyView")
        super.onDestroyView()
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        Log.d("BottomNavFragment", "onCreateView")
        val v = inflater.inflate(R.layout.fragment_bottomsheet, container, false)
        mapNavView = v.findViewById(R.id.nav_view)
        mapNavView.setNavigationItemSelectedListener { menuItem ->
            // Bottom Navigation Drawer menu item clicks
            clickListener.onItemClick(menuItem.itemId)
            true
        }
        return v
    }

}

frag_bottomsheet. xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:fitsSystemWindows="true"
  android:layout_width="match_parent"
  android:layout_height="wrap_content">


  <com.google.android.material.navigation.NavigationView
    android:id="@+id/nav_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:menu="@menu/bottom_nav_drawer_menu" />

</androidx.constraintlayout.widget.ConstraintLayout>

bottom_nav_drawer_menu. xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
  <group android:checkableBehavior="none">
    <item
      android:id="@+id/nav_item_1"
      android:title="@string/item_1" />
    <item
      android:id="@+id/nav_item_2"
      android:title="@string/item_2" />
    <item
      android:id="@+id/nav_item_3"
      android:title="@string/item_3" />
    <item
      android:id="@+id/nav_item_4"
      android:title="@string/item_4" />
    <item
      android:id="@+id/preferences"
      android:title="@string/preferences" />
  </group>
</menu>

Любая помощь будет принята с благодарностью. Я уверен, что есть кое-что, что я пропускаю, но я предполагаю, что я смотрел на это так долго, я смотрю мимо этого.

-Rindress

1 Ответ

2 голосов
/ 21 февраля 2020

Ключевая часть прослеживаемой трассы - это где ~~~:

├─ com.example.testleak.MainActivity instance
│    Leaking: NO (Activity#mDestroyed is false)
│    ↓ MainActivity.bottomNavFragment
│                   ~~~~~~~~~~~~~~~~~
╰→ com.example.testleak.BottomNavigationDrawerFragment instance
​     Leaking: YES (ObjectWatcher was watching this because
com.example.testleak.BottomNavigationDrawerFragment received Fragment#onDestroy() callback
and Fragment#mFragmentManager is null)

Это говорит нам о том, что MainActivity не уничтожено, а BottomNavigationDrawerFragment. Когда BottomNavigationDrawerFragment становится уничтоженным, его следует собирать. Однако его нельзя собирать, поскольку MainActivity хранит ссылку на него в MainActivity.bottomNavFragment

Когда MainActivity.clickListener вызывает bottomNavFragment?.dismiss(), ему также следует установить bottomNavFragment на null. И вместо установки MainActivity.bottomNavFragment для нового экземпляра в MainActivity.onCreate(), новый экземпляр должен быть создан при отображении фрагмента, например, в MainActivity.onOptionsItemSelected

...