RecyclerView↑ 正在泄漏并且 View 分离并且有父级

RecyclerView↑ is leaking and View detached and has parent

我尝试在 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 instance
│    Leaking: NO (TopicFragment↓ is not leaking)
│    Anonymous subclass of androidx.activity.result.ActivityResultRegistry
│    ↓ ComponentActivity.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 instance
│    Leaking: NO (TopicFragment↓ is not leaking)
│    Anonymous class implementing androidx.activity.result.ActivityResultCallback
│    ↓ FragmentManager.this[=10=]
├─ 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 保留了应该被 GC 处理的片段分离视图。

此值是通过调用 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