NavContoller 片段泄漏

NavContoller Leaking in Fragment

当我使用 JetPack Navigation 在片段之间转换时,我无法找到发生泄漏的原因。

查看下面来自泄漏金丝雀的堆栈跟踪

D/LeakCanary: ​
    ┬───
    │ GC Root: System class
    │
    ├─ android.view.inputmethod.InputMethodManager class
    │    Leaking: NO (InputMethodManager↓ is not leaking and a class is never leaking)
    │    ↓ static InputMethodManager.sInstance
    ├─ android.view.inputmethod.InputMethodManager instance
    │    Leaking: NO (DecorView↓ is not leaking and InputMethodManager is a singleton)
    │    ↓ InputMethodManager.mNextServedView
    ├─ com.android.internal.policy.DecorView instance
    │    Leaking: NO (LinearLayout↓ is not leaking and View attached)
    │    View is part of a window view hierarchy
D/LeakCanary: │    View.mAttachInfo is not null (view attached)
    │    View.mWindowAttachCount = 1
    │    mContext instance of com.android.internal.policy.DecorContext, wrapping activity com.carepay.flows.CarePayActivity
    │    with mDestroyed = false
    │    ↓ DecorView.mContentRoot
    ├─ android.widget.LinearLayout instance
    │    Leaking: NO (CarePayActivity↓ is not leaking and View attached)
    │    View is part of a window view hierarchy
    │    View.mAttachInfo is not null (view attached)
    │    View.mWindowAttachCount = 1
    │    mContext instance of com.carepay.flows.CarePayActivity with mDestroyed = false
    │    ↓ LinearLayout.mContext
    ├─ com.carepay.flows.CarePayActivity instance
    │    Leaking: NO (NavHostFragment↓ is not leaking and Activity#mDestroyed is false)
    │    mApplication instance of com.carepay.MemberApp
    │    mBase instance of androidx.appcompat.view.ContextThemeWrapper, not wrapping known Android context
    │    ↓ CarePayActivity.mFragments
    ├─ androidx.fragment.app.FragmentController instance
    │    Leaking: NO (NavHostFragment↓ is not leaking)
    │    ↓ FragmentController.mHost
    ├─ androidx.fragment.app.FragmentActivity$HostCallbacks instance
    │    Leaking: NO (NavHostFragment↓ is not leaking)
    │    this[=10=] instance of com.carepay.flows.CarePayActivity with mDestroyed = false
    │    mActivity instance of com.carepay.flows.CarePayActivity with mDestroyed = false
    │    mContext instance of com.carepay.flows.CarePayActivity with mDestroyed = false
    │    ↓ FragmentActivity$HostCallbacks.mFragmentManager
D/LeakCanary: ├─ androidx.fragment.app.FragmentManagerImpl instance
    │    Leaking: NO (NavHostFragment↓ is not leaking)
    │    ↓ FragmentManagerImpl.mPrimaryNav
    ├─ androidx.navigation.fragment.NavHostFragment instance
    │    Leaking: NO (Fragment#mFragmentManager is not null)
    │    ↓ NavHostFragment.mNavController
    │                      ~~~~~~~~~~~~~~
    ├─ androidx.navigation.NavHostController instance
    │    Leaking: UNKNOWN
    │    Retaining 1313968 bytes in 7994 objects
    │    mActivity instance of com.carepay.flows.CarePayActivity with mDestroyed = false
    │    mContext instance of com.carepay.flows.CarePayActivity with mDestroyed = false
    │    ↓ NavHostController.mOnDestinationChangedListeners
    │                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ├─ java.util.concurrent.CopyOnWriteArrayList instance
    │    Leaking: UNKNOWN
    │    Retaining 1302920 bytes in 7681 objects
    │    ↓ CopyOnWriteArrayList.elements
    │                           ~~~~~~~~
    ├─ java.lang.Object[] array
    │    Leaking: UNKNOWN
    │    Retaining 1302896 bytes in 7679 objects
    │    ↓ Object[].[2]
D/LeakCanary: │               ~~~
    ├─ androidx.navigation.ui.ActionBarOnDestinationChangedListener instance
    │    Leaking: UNKNOWN
    │    Retaining 5425 bytes in 190 objects
    │    mActivity instance of com.carepay.flows.CarePayActivity with mDestroyed = false
    │    mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
    │    activity com.carepay.flows.CarePayActivity with mDestroyed = false
    │    ↓ ActionBarOnDestinationChangedListener.mContext
    │                                            ~~~~~~~~
    ├─ dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper instance
    │    Leaking: UNKNOWN
    │    Retaining 4365 bytes in 162 objects
    │    mBase instance of com.carepay.flows.CarePayActivity with mDestroyed = false
    │    ViewComponentManager$FragmentContextWrapper wraps an Activity with Activity.mDestroyed false
    │    ↓ ViewComponentManager$FragmentContextWrapper.fragment
    │                                                  ~~~~~~~~
    ╰→ com.carepay.flows.treatmentdetail.TreatmentDetailFragment instance
    ​     Leaking: YES (ObjectWatcher was watching this because com.carepay.flows.treatmentdetail.TreatmentDetailFragment
    ​     received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
    ​     Retaining 3565 bytes in 129 objects
    ​     key = 3b650fce-861c-463b-8ccb-78255dfc535a
    ​     watchDurationMillis = 22434
    ​     retainedDurationMillis = 16751
    ​     componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
    ​     wrapping activity com.carepay.flows.CarePayActivity with mDestroyed = false

所有片段转换都会引发此泄漏,因此我将分享所选堆栈跟踪中片段的片段。

TreatmentDetailFragment

class TreatmentDetailFragment : OverlayFragment(R.layout.fragment_treatment_detail) {

    private val binding get() = (_binding as FragmentTreatmentDetailBinding?)!!
    private val args: TreatmentDetailFragmentArgs by navArgs()
    private val viewModel by viewModels<TreatmentDetailViewModel>()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentTreatmentDetailBinding.inflate(inflater,  container, false)

        val adapter = ItemsAdapter()
        binding.treatmentItems.adapter = adapter

        setupObservers(adapter)
        viewModel.fetchTreatment(args.treatmentId, lifecycle)

        return binding.root
    }

    private fun setupObservers(adapter: ItemsAdapter) {
        viewModel.treatment.observe(viewLifecycleOwner){ treatment ->
            binding.titleView.text = getString(R.string.treatment, treatment.treatmentCode)
            binding.treatment = treatment
            adapter.items = treatment.getItems()
            adapter.notifyDataSetChanged()
        }
    }
}

OverlayFragment

abstract class OverlayFragment(@LayoutRes layoutResource: Int) : BaseFragment(layoutResource) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val activity = requireActivity() as CarePayActivity
        activity.showBackButton(true)
        activity.showCloseIcon(true)
        activity.showBottomNavigation(View.GONE)
    }
}

基础片段

@AndroidEntryPoint
abstract class BaseFragment(@LayoutRes layoutResource: Int) : Fragment(layoutResource) {
    @Inject
    protected lateinit var memberSettingsRepository: MemberSettingsRepository
    protected var _binding: ViewBinding? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val navController = findNavController()
        val appBarConfiguration = AppBarConfiguration(
            setOf(
                R.id.navigation_health,
                R.id.navigation_benefits,
                R.id.navigation_clinics,
                R.id.navigation_account
            )
        )
        val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
        val activity = requireActivity() as CarePayActivity
        activity.setSupportActionBar(toolbar)

        activity.setupActionBarWithNavController(navController, appBarConfiguration)
    }
}

Activity

@AndroidEntryPoint
class CarePayActivity : AppCompatActivity() {

    @Inject
    lateinit var memberSettingsRepository: MemberSettingsRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_carepay)
        val navView: BottomNavigationView = findViewById(R.id.nav_view)

        val navController = findNavController(R.id.nav_host_fragment)
        val inflater = navController.navInflater
        val graph = inflater.inflate(R.navigation.mobile_navigation)

        when {
            intent.hasExtra(RESUMED) -> {
                graph.startDestination = R.id.navigation_health
            }
            else -> {
                graph.startDestination = R.id.navigation_setup
            }
        }
        navController.graph = graph
        navView.setupWithNavController(navController)

    }

    override fun onSupportNavigateUp(): Boolean {
        return findNavController(R.id.nav_host_fragment).navigateUp() || super.onSupportNavigateUp()
    }

    override fun onBackPressed() {
        val currentDestination = findNavController(R.id.nav_host_fragment).currentDestination
        when (currentDestination?.id) {
            R.id.navigation_health -> {
                val dialog = getDialog(
                    getString(R.string.exit),
                    getString(R.string.exit_message, getString(R.string.app_name)),
                    getString(R.string.yes),
                    getString(R.string.no),
                    object : DialogButtonEvents {
                        override fun onButtonClicked(id: Int) {
                            if (id == R.id.positiveButton) {
                                finish()
                            }
                        }

                    })
                dialog.show()
                dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT));
            }
            else -> {
                findNavController(R.id.nav_host_fragment).navigateUp()
            }
        }
    }
}

注意:碎片比他们的观点更有效。确保清除片段的 onDestroyView() 方法中对绑定 class 实例的所有引用。 link

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}
 val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
        val activity = requireActivity() as CarePayActivity
        activity.setSupportActionBar(toolbar)

        activity.setupActionBarWithNavController(navController, appBarConfiguration)

这些行会导致泄漏,因为您将属于 FragmentnavController 提供给 Activity,因此每当 Fragment 被销毁时,它都会由于 navController.

无法取消引用