android - RecyclerView↑ 发生泄漏,View 已分离并具有父级

标签 android leakcanary

我尝试在 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 保留应该已被 GC 的 fragment 分离 View 。

该值是通过调用 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 fragment 库提出问题。

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

关于android - RecyclerView↑ 发生泄漏,View 已分离并具有父级,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66350771/

相关文章:

java - 如何通过ListAdpter显示多张图片?

java - Android 中的 jdbc 问题 - 无法连接到数据库

java - 为什么这个 Observable.timer() 会导致内存泄漏?

android - 添加 fragment 并调用 .replace() 导致添加的 fragment 中的内存泄漏

java - 匿名实现导致内存泄漏

android - 使用 fragment 和导航组件泄漏 nestedscrollView

android - 未能完成 Gradle 执行(新项目)-java home 不同

android - 在 android gradle 中导入 itext-7

android - 递归调用 getWritableDatabase

android - LeakCanary 构建失败,构建类型不是调试或发布