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
我尝试在 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