RecyclerView↑ 正在泄漏,View 已分离并具有父级

问题描述

我尝试在 onDestroyView 中设置适配器 null 也尝试使用 addOnAttachStatechangelistener 但仍然存在内存泄漏。

这是我的堆栈跟踪

┬───
│ 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
D/LeakCanary: ├─ com.ics.homework.ui.MainActivity instance
│    Leaking: NO (TopicFragment↓ is not leaking and Activity#mDestroyed is false)
│    ↓ MainActivity.mActivityResultRegistry
├─ androidx.activity.ComponentActivity$2 instance
│    Leaking: NO (TopicFragment↓ is not leaking)
│    Anonymous subclass of androidx.activity.result.ActivityResultRegistry
│    ↓ ComponentActivity$2.mKeyToCallback
├─ java.util.HashMap instance
│    Leaking: NO (TopicFragment↓ is not leaking)
│    ↓ HashMap.table
├─ java.util.HashMap$HashMapEntry[] array
│    Leaking: NO (TopicFragment↓ is not leaking)
│    ↓ HashMap$HashMapEntry[].[1]
├─ java.util.HashMap$HashMapEntry instance
│    Leaking: NO (TopicFragment↓ is not leaking)
│    ↓ HashMap$HashMapEntry.value
├─ androidx.activity.result.ActivityResultRegistry$CallbackAndContract instance
│    Leaking: NO (TopicFragment↓ is not leaking)
│    ↓ ActivityResultRegistry$CallbackAndContract.mCallback
├─ androidx.fragment.app.FragmentManager$10 instance
│    Leaking: NO (TopicFragment↓ is not leaking)
│    Anonymous class implementing androidx.activity.result.ActivityResultCallback
│    ↓ FragmentManager$10.this$0
├─ androidx.fragment.app.FragmentManagerImpl instance
│    Leaking: NO (TopicFragment↓ is not leaking)
│    ↓ FragmentManagerImpl.mParent
├─ com.ics.homework.ui.course.topics.TopicFragment instance
│    Leaking: NO (Fragment#mFragmentManager is not null)
│    ↓ TopicFragment.mAnimationInfo
│                    ~~~~~~~~~~~~~~
├─ androidx.fragment.app.Fragment$AnimationInfo instance
│    Leaking: UNKNowN
│    ↓ Fragment$AnimationInfo.mFocusedView
│                             ~~~~~~~~~~~~
├─ androidx.recyclerview.widget.RecyclerView instance
│    Leaking: YES (View detached and has parent)
│    mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextwrapper,wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
│    View#mParent is set
│    View#mAttachInfo is null (view detached)
│    View.mID = R.id.recyclerView
│    View.mWindowAttachCount = 1
│    ↓ RecyclerView.mParent
├─ androidx.swiperefreshlayout.widget.SwipeRefreshLayout instance
│    Leaking: YES (RecyclerView↑ is leaking and View detached and has parent)
│    mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextwrapper,wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
│    View#mParent is set
│    View#mAttachInfo is null (view detached)
│    View.mID = R.id.swipeRefreshLayout
│    View.mWindowAttachCount = 1
│    ↓ SwipeRefreshLayout.mParent
D/LeakCanary: ╰→ androidx.constraintlayout.widget.ConstraintLayout instance
​     Leaking: YES (ObjectWatcher was watching this because com.ics.homework.ui.course.topics.TopicFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
​     key = b5fcd20b-86ca-432c-ab69-0e4a90881651
​     watchDurationMillis = 26087
​     retainedDurationMillis = 21084
​     mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextwrapper,wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
​     View#mParent is null
​     View#mAttachInfo is null (view detached)
​     View.mWindowAttachCount = 1
====================================
0 LIBRARY LEAKS

我的实现是

@AndroidEntryPoint
class TopicFragment : Fragment() {
private var courseId: String? = null
private var title: String? = null
private var _binding: FragmentTopicBinding? = null
private val binding get() = _binding!!
private val topicviewmodel by viewmodels<Topicviewmodel>()
private lateinit var topicAdapter: TopicAdapter

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    arguments?.let {
        courseId = it.getString(ARG_COURSE_ID)
        title = it.getString(ARG_TITLE)
    }
}

override fun onCreateView(
    inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?
): View {
    _binding = FragmentTopicBinding.inflate(inflater,container,false)
    return binding.root
}

override fun onViewCreated(view: View,savedInstanceState: Bundle?) {
    super.onViewCreated(view,savedInstanceState)
    binding.lifecycleOwner = viewLifecycleOwner
    binding.viewmodel = topicviewmodel

    topicAdapter = TopicAdapter(topicItemClick)

    binding.apply {
        stateErrorView.apply {
            handler = retryCallback
        }
        swipeRefreshLayout.setonRefreshListener {
            topicviewmodel.retry()
        }
        recyclerView.apply {
            layoutManager = linearlayoutmanager(requireContext(),linearlayoutmanager.VERTICAL,false)
            addItemdecoration(DividerItemdecoration(requireContext(),DividerItemdecoration.VERTICAL))
            adapter = topicAdapter
            setHasFixedSize(true)
        }
    }
    observeUI()
}

private fun observeUI() {
    topicviewmodel.topics.observe(viewLifecycleOwner,{
        Timber.e(it.status.toString())
        it.data?.let(topicAdapter::submitList)
        if (it.status == Status.ERROR) topicAdapter.submitList(listof())
        if (it.status != Status.LOADING) binding.swipeRefreshLayout.isRefreshing = false
    })
}

private val topicItemClick = object : TopicItemClick {
    override fun onClick(topic: Topic) {
        val action = TopicFragmentDirections.actionTopicFragmenttochapterFragment(
            topic.postId,title!!,false,topic.id,topic.topic
        )
        findNavController().navigate(action)
    }
}
private val retryCallback = object : RetryCallback {
    override fun retry() {
        topicviewmodel.retry()
    }
}

override fun onDestroyView() {
    binding.recyclerView.addOnAttachStatechangelistener(object : View.OnAttachStatechangelistener {
        override fun onViewAttachedToWindow(v: View) {}
        override fun onViewDetachedFromWindow(v: View) {
            binding.recyclerView.adapter=null
        }
    })
    super.onDestroyView()
    _binding = null
}

companion object {
    private const val ARG_COURSE_ID = "courseId"
    private const val ARG_TITLE = "title"
}
}

解决方法

TopicFragment 处于创建状态,Fragment.mAnimationInfo 保留一个 Fragment$AnimationInfo 并且 Fragment$AnimationInfo.mFocusedView 保留应该已经 GCed 的 Fragment 分离视图。

此值是通过调用 Fragment.setFocusView() 设置的,快速搜索显示从未清除过的值:https://cs.android.com/search?q=setFocusedView&sq=&ss=androidx%2Fplatform%2Fframeworks%2Fsupport

这个变化似乎是在 2020 年引入的:https://cs.android.com/androidx/platform/frameworks/support/+/1052c3662c40176f7f02da9e06b989dcab21d500

最好是针对 androidx 片段库提出问题。

实际上已经提交了,在下一个版本中修复:https://issuetracker.google.com/issues/179925887