Fragment 中的 setHasOptionsMenu 导致 MEMORY LEAK

Posted

技术标签:

【中文标题】Fragment 中的 setHasOptionsMenu 导致 MEMORY LEAK【英文标题】:setHasOptionsMenu in Fragment causes MEMORY LEAK 【发布时间】:2020-06-30 20:43:32 【问题描述】:

我不知道为什么,但是在片段中的 onCreate 中调用 setHasMenuOption(true) 会导致内存泄漏。

我尝试在 onDestroyView 中调用 setHasMenuOption(false),但没有成功。

不调用 setHasMenuOption() 不会导致任何内存泄漏..

如何避免这种泄漏??

我使用 Fragment 作为容器来托管其他 Fragment。

Host Fragment 类 --

  override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setHasOptionsMenu(true)

    

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


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

        val actionBarDrawerToggle = ActionBarDrawerToggle(
            activity,
            drawerLayout,
            toolbar,
            R.string.open_drawer,
            R.string.close_drawer
        )


        drawerLayout.addDrawerListener(actionBarDrawerToggle)
        actionBarDrawerToggle.syncState()


        setToolbarTitle("Restaurants")


        setNavigationItemClicks()

       //calling fragment HOME_FRAGMENT within container fragment
        childFragmentManager.beginTransaction()
            .replace(R.id.nestedFragmentsContainer, HomeFragment(), FragmentsTag.HOME_FRAGMENT)
            .commit()
           // setting container fragment as primary navigation fragment
        fragmentManager?.beginTransaction()?.setPrimaryNavigationFragment(this)?.commit()

    


    override fun onOptionsItemSelected(item: MenuItem): Boolean 
       return if (item.itemId == android.R.id.home) 
            drawerLayout.openDrawer(GravityCompat.START)
            true
        else
           false
       
    


    private fun setNavigationItemClicks() 
        navigationView.setNavigationItemSelectedListener 
            when (it.itemId) 
                R.id.home -> 
                    changeFragment(HomeFragment(), FragmentsTag.HOME_FRAGMENT)
                    setToolbarTitle("Restaurants")
                
                R.id.profile -> 
                    changeFragment(
                        ProfileFragment(),
                        FragmentsTag.PROFILE_FRAGMENT
                    )
                    setToolbarTitle("Profile")
                
                R.id.favorites -> 
                    changeFragment(
                        FavoritesFragment(),
                        FragmentsTag.FAVORITES_FRAGMENT
                    )
                    setToolbarTitle("Favorites")
                
                R.id.faq -> 
                    changeFragment(FaqFragment(), FragmentsTag.FAQ_FRAGMENT)
                    setToolbarTitle("FAQs")
                
                R.id.logout -> showLogoutConfirmationDialog()
            
            return@setNavigationItemSelectedListener true
        
    


    private fun setToolbarTitle(title: String) 
        val activity = (activity as AppCompatActivity)
        activity.setSupportActionBar(toolbar)
        activity.supportActionBar?.title = title

    


    private fun changeFragment(fragment: Fragment, tag: String) 
        drawerLayout.closeDrawer(GravityCompat.START)
        childFragmentManager.beginTransaction()
            .replace(R.id.nestedFragmentsContainer, fragment, tag)
            .commit()
    

    private fun showLogoutConfirmationDialog() 
        drawerLayout.closeDrawer(GravityCompat.START)
        MaterialAlertDialogBuilder(activity as Context, R.style.AlertDialogTheme).apply 
            setTitle("Logout")
            setMessage("Are you sure?")
            setPositiveButton("Ok")  _, _ ->
                logout()
            

            setNegativeButton("Cancel")  dialog: DialogInterface?, _: Int -> dialog?.dismiss() 
        .show()
    

    private fun logout() 
        clearSharedPref()
        fragmentManager?.apply 
            popBackStack(
                findFragmentById(R.id.fragmentContainer)?.tag,
                FragmentManager.POP_BACK_STACK_INCLUSIVE
            )
            beginTransaction().apply 
                setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
                replace(R.id.fragmentContainer, LoginFragment(), FragmentsTag.LOGIN_FRAGMENT)
                addToBackStack(FragmentsTag.LOGIN_FRAGMENT)
            .commit()
        
    

    private fun clearSharedPref() 
        activity?.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE)?.apply 
            edit().clear().apply()
        
    

    override fun onDestroyView() 
        super.onDestroyView()
        clearFindViewByIdCache()
        setHasOptionsMenu(false)
          fragmentManager?.apply 
              beginTransaction().setPrimaryNavigationFragment(null).commit()
        
        (activity as AppCompatActivity).setSupportActionBar(null)
    

LeakCanary 泄漏跟踪--

 ┬───
    │ GC Root: System class
    │
    ├─ leakcanary.internal.InternalLeakCanary class
    │    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
    │    ↓ static InternalLeakCanary.resumedActivity
    ├─ com.example.foodrunner.activities.MainActivity instance
    │    Leaking: NO (Activity#mDestroyed is false)
    │    ↓ MainActivity.mFragments
    │                   ~~~~~~~~~~
    ├─ androidx.fragment.app.FragmentController instance
    │    Leaking: UNKNOWN
    │    ↓ FragmentController.mHost
    │                         ~~~~~
    ├─ androidx.fragment.app.FragmentActivity$HostCallbacks instance
    │    Leaking: UNKNOWN
    │    ↓ FragmentActivity$HostCallbacks.mFragmentManager
    │                                     ~~~~~~~~~~~~~~~~
    ├─ androidx.fragment.app.FragmentManagerImpl instance
    │    Leaking: UNKNOWN
    │    ↓ FragmentManagerImpl.mCreatedMenus
    │                          ~~~~~~~~~~~~~
    ├─ java.util.ArrayList instance
    │    Leaking: UNKNOWN
    │    ↓ ArrayList.elementData
    │                ~~~~~~~~~~~
    ├─ java.lang.Object[] array
    │    Leaking: UNKNOWN
    │    ↓ Object[].[0]
    │               ~~~
    ╰→ com.example.foodrunner.fragments.NestedFragmentsContainerFragment instance
    ​     Leaking: YES (ObjectWatcher was watching this because com.example.foodrunner.fragments.NestedFragmentsContainerFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
    ​     key = 22ecd71d-dbcd-470f-9ca3-0c992c1aada3
    ​     watchDurationMillis = 11383
    ​     retainedDurationMillis = 6382
    ​     key = adac2a58-8e2a-45f4-850b-f87ed815c9ce
    ​     watchDurationMillis = 11384
    ​     retainedDurationMillis = 6383
    ====================================

【问题讨论】:

你使用的是什么版本的 Fragments? @ianhanniballake 老一个。不使用导航组件。 喜欢...几岁? AndroidX 之前的版本?片段 1.0.0? 1.1.0? @ianhanniballake 我不知道片段版本。但我的项目是使用 androidx 依赖项和目标 api 29 构建的。 当你show your dependencies时,那是什么意思? 【参考方案1】:

我有同样的问题,在我的情况下,将 Fragment 版本更新为 1.2.3 并没有解决内存泄漏问题。

class ExampleFragment : Fragment() 
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? 
        return inflater.inflate(R.layout.fragment_example, container, false)
    

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) 
        super.onViewCreated(view, savedInstanceState)
        configureToolbar(detailView.toolbar)
        setHasOptionsMenu(true) ### This causes the memory leak
    

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) 
        inflater.inflate(R.menu.detail_menu_example, menu)
    

    private fun configureToolbar(toolbar: Toolbar) 
        (requireActivity() as AppCompatActivity).run  setSupportActionBar(toolbar) 
        (requireActivity() as AppCompatActivity).supportActionBar.let 
            it?.setDisplayHomeAsUpEnabled(true)
            it?.setHomeButtonEnabled(true)
        
        toolbar.setNavigationOnClickListener 
            parentFragment?.apply 
                childFragmentManager.popBackStack()
            
        
    

    override fun onDestroyView() 
        super.onDestroyView()
        clearFindViewByIdCache()
        setHasOptionsMenu(false)
        fragmentManager?.apply 
            beginTransaction().setPrimaryNavigationFragment(null).commit()
        
        (activity as AppCompatActivity).setSupportActionBar(null)
    

我的片段版本:

$ ./gradlew ui:androidDependencies | grep -i "fragment"
+--- androidx.fragment:fragment:1.2.3@aar
+--- androidx.fragment:fragment:1.2.3@aar
+--- androidx.fragment:fragment:1.2.3@aar
+--- androidx.fragment:fragment:1.2.3@aar
+--- androidx.fragment:fragment:1.2.3@aar
+--- androidx.fragment:fragment:1.2.3@aar
+--- androidx.fragment:fragment:1.2.3@aar
+--- androidx.fragment:fragment:1.2.3@aar
+--- androidx.fragment:fragment:1.2.3@aar
+--- androidx.fragment:fragment:1.2.3@aar

【讨论】:

以上是关于Fragment 中的 setHasOptionsMenu 导致 MEMORY LEAK的主要内容,如果未能解决你的问题,请参考以下文章

Fragment中的ViewPager的Fragment添加子Fragment要放在onAttach中添加

Fragment系列:布局xml中的fragment标签,如何隐藏?

Fragment 中的 Fragment 不能保留它们的值

android ViewPager+Fragment 如何在ViewPager的Activity中获取Fragment中的控件对象

ViewPager+TabLayout+Fragment刷新Fragment中的数据

从 Fragment B 中的 Fragment A 访问视图