安卓系统:具有分页3流的ViewModel正在泄漏



我的问题是,我的shopViewModel(它包含paging-flow的实例(以某种方式泄漏。我试图通过将flow转换为livedata来解决这个问题,但这并没有改变。

ViewModel

class ShopViewModel @ViewModelInject constructor(
private val shopPagingSource: ShopPagingSource,
) : ViewModel() {
val SHOP_PAGE_CONFIG: PagingConfig = PagingConfig(pageSize = 20, enablePlaceholders = false)
// As LiveData
val shopFlow = Pager(SHOP_PAGE_CONFIG) { shopPagingSource }.flow.cachedIn(viewModelScope).asLiveData()
// Before
val shopFlow = Pager(SHOP_PAGE_CONFIG) { shopPagingSource }.flow.cachedIn(viewModelScope)
}

碎片

@AndroidEntryPoint
class ShopFragment(private val shopListAdapter: ShopAdapter) : Fragment(R.layout.fragment_shop), ShopAdapter.OnItemClickListener {
private val shopViewModel: ShopViewModel by viewModels()
private val shopBinding: FragmentShopBinding by viewBinding()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shopBinding.adapter = shopListAdapter.withLoadStateFooter(ShopLoadAdapter(shopListAdapter::retry))
shopListAdapter.clickHandler(this)
collectShopList()
}

override fun forwardClick(product: @NotNull Product) {
val action = ShopFragmentDirections.actionShopFragmentToShopItemFragment(product)
findNavController().navigate(action)
}

private fun collectShopListWithLiveData() = lifecycleScope.launch {
shopViewModel.shopFlow.observe(viewLifecycleOwner) {
lifecycleScope.launch {
shopListAdapter.submitData(it)
}
}
}
// Before converting to livedata
private fun collectShopListWithFlow() = lifecycleScope.launch {
shopViewModel.shopFlow.collectLatest {
shopListAdapter.submitData(it)
}
}

// To avoid memory leak from injected adapter
override fun onDestroyView() {
requireView().findViewById<RecyclerView>(R.id.rv_shop).adapter = null
super.onDestroyView()
}
}

适配器

class ShopAdapter @Inject constructor() : PagingDataAdapter<Product, ShopAdapter.ShopViewHolder>(Companion) {
private lateinit var clickListener: OnItemClickListener
companion object: DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(oldItem: Product, newItem: Product): Boolean = oldItem.articelNumber == newItem.articelNumber
override fun areContentsTheSame(oldItem: Product, newItem: Product): Boolean = oldItem == newItem
}
inner class ShopViewHolder(val binding: ShopListItemBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShopAdapter.ShopViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ShopListItemBinding.inflate(layoutInflater, parent, false)
return ShopViewHolder(binding).also {
binding.mcvProductItem.setOnClickListener { clickListener.forwardClick(binding.product!!) }
}
}
override fun onBindViewHolder(holder: ShopAdapter.ShopViewHolder, position: Int) {
holder.binding.product = getItem(position) ?: return
holder.binding.executePendingBindings()
}
fun clickHandler(clickEventHandler: OnItemClickListener) {
clickListener = clickEventHandler
}
interface OnItemClickListener {
fun forwardClick(product: @NotNull Product)
}
}

主碎片工厂

class MainFragmentFactory @Inject constructor(
// .. other dependencies
private val shopAdapter: ShopAdapter,
) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment = when(className) {
// ... other fragments
ShopFragment::class.java.name -> ShopFragment(shopAdapter)
else -> super.instantiate(classLoader, className)
}

寻呼源

class ShopPagingSource @Inject constructor(
private val shopRepository: ShopFirebaseRepository,
) : PagingSource<QuerySnapshot, Product>() {
override suspend fun load(params: LoadParams<QuerySnapshot>): LoadResult<QuerySnapshot, Product> = try {
withTimeout(SHOP_MAX_LOADING_TIME) {
val currentPage = params.key ?: shopRepository.getCurrentPage()
val lastDocumentSnapShot = currentPage.documents[currentPage.size() - 1]
val nextPage = shopRepository.getNextPage(lastDocumentSnapShot)
LoadResult.Page(
data = currentPage.toObjects(),
prevKey = null,
nextKey = nextPage
)
}
} catch (e: TimeoutCancellationException) {
Timber.d("Mediator failed, No Internet Connection")
LoadResult.Error(e)
} catch (e: ArrayIndexOutOfBoundsException) {
Timber.d("Mediator failed, ArrayIndexOutOfBounds")
LoadResult.Error(e)
} catch (e: Exception) {
Timber.d("Mediator failed, Unknown Error: ${e.message.toString()}")
LoadResult.Error(e)
}
}

LeakCanary

D/LeakCanary: ====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS

References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.

2618 bytes retained by leaking objects
Signature: 944313b4ecbdb77c99682dc8c1646e12e4f37d8
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
│    Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
│    ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│    Leaking: NO (InternalLeakCanary↓ is not leaking)
│    ↓ Object[].[2142]
├─ leakcanary.internal.InternalLeakCanary class
│    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│    ↓ static InternalLeakCanary.resumedActivity
├─ com.example.app.framework.ui.view.MainActivity instance
│    Leaking: NO (MainNavHostFragment↓ is not leaking and Activity#mDestroyed is false)
│    ↓ MainActivity.navController$delegate
├─ kotlin.SynchronizedLazyImpl instance
│    Leaking: NO (MainNavHostFragment↓ is not leaking)
│    ↓ SynchronizedLazyImpl._value
├─ androidx.navigation.NavHostController instance
│    Leaking: NO (MainNavHostFragment↓ is not leaking)
│    ↓ NavHostController.mLifecycleOwner
├─ com.example.app.framework.ui.view.utils.MainNavHostFragment instance
│    Leaking: NO (Fragment#mFragmentManager is not null)
│    ↓ MainNavHostFragment.mainFragmentFactory
│                          ~~~~~~~~~~~~~~~~~~~
├─ com.example.app.framework.ui.view.utils.MainFragmentFactory instance
│    Leaking: UNKNOWN
│    ↓ MainFragmentFactory.shopAdapter
│                          ~~~~~~~~~~~
├─ com.example.app.framework.ui.adapter.recyclerview.ShopAdapter instance
│    Leaking: UNKNOWN
│    ↓ ShopAdapter.differ
│                  ~~~~~~
├─ androidx.paging.AsyncPagingDataDiffer instance
│    Leaking: UNKNOWN
│    ↓ AsyncPagingDataDiffer.differBase
│                            ~~~~~~~~~~
├─ androidx.paging.AsyncPagingDataDiffer$differBase$1 instance
│    Leaking: UNKNOWN
│    Anonymous subclass of androidx.paging.PagingDataDiffer
│    ↓ AsyncPagingDataDiffer$differBase$1.receiver
│                                         ~~~~~~~~
├─ androidx.paging.PageFetcher$PagerUiReceiver instance
│    Leaking: UNKNOWN
│    ↓ PageFetcher$PagerUiReceiver.this$0
│                                  ~~~~~~
├─ androidx.paging.PageFetcher instance
│    Leaking: UNKNOWN
│    ↓ PageFetcher.pagingSourceFactory
│                  ~~~~~~~~~~~~~~~~~~~
├─ com.example.app.framework.ui.viewmodel.ShopViewModel$shopFlow$1 instance
│    Leaking: UNKNOWN
│    Anonymous subclass of kotlin.jvm.internal.Lambda
│    ↓ ShopViewModel$shopFlow$1.this$0
│                               ~~~~~~
╰→ com.example.app.framework.ui.viewmodel.ShopViewModel instance
​     Leaking: YES (ObjectWatcher was watching this because com.example.app.framework.ui.viewmodel.ShopViewModel received ViewModel#onCleared() callback)
​     key = 0e65fcab-e6dd-475a-83d4-87b2050d797b
​     watchDurationMillis = 7771
​     retainedDurationMillis = 2769
====================================

编辑

当用@FragmentScoped界定ShopAdapter时,我得到以下泄漏:

┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
│    Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
│    ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│    Leaking: NO (InternalLeakCanary↓ is not leaking)
│    ↓ Object[].[409]
├─ leakcanary.internal.InternalLeakCanary class
│    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│    ↓ static InternalLeakCanary.resumedActivity
├─ com.example.app.framework.ui.view.MainActivity instance
│    Leaking: NO (MainNavHostFragment↓ is not leaking and Activity#mDestroyed is false)
│    mApplication instance of com.example.app.App
│    mBase instance of androidx.appcompat.view.ContextThemeWrapper, not wrapping known Android context
│    ↓ MainActivity.navController$delegate
├─ kotlin.SynchronizedLazyImpl instance
│    Leaking: NO (MainNavHostFragment↓ is not leaking)
│    ↓ SynchronizedLazyImpl._value
├─ androidx.navigation.NavHostController instance
│    Leaking: NO (MainNavHostFragment↓ is not leaking)
│    mActivity instance of com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│    mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
│    activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│    ↓ NavHostController.mLifecycleOwner
├─ com.example.app.framework.ui.view.utils.MainNavHostFragment instance
│    Leaking: NO (Fragment#mFragmentManager is not null)
│    componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
│    wrapping activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│    ↓ MainNavHostFragment.mainFragmentFactory
│                          ~~~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ com.example.app.framework.ui.view.utils.MainFragmentFactory instance
│    Leaking: UNKNOWN
│    Retaining 212 bytes in 7 objects
│    ↓ MainFragmentFactory.shopAdapter
│                          ~~~~~~~~~~~
├─ com.example.app.framework.ui.adapter.recyclerview.ShopAdapter instance
│    Leaking: UNKNOWN
│    Retaining 14461 bytes in 546 objects
│    ↓ ShopAdapter.clickListener
│                  ~~~~~~~~~~~~~
╰→ com.example.app.framework.ui.view.fragments.shop.ShopFragment instance
​     Leaking: YES (ObjectWatcher was watching this because com.example.app.framework.ui.view.fragments.shop.
​     ShopFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
​     Retaining 2121 bytes in 79 objects
​     key = 71ec5094-8509-47a5-9e0a-070fe642ca8a
​     watchDurationMillis = 18366
​     retainedDurationMillis = 13365
​     componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
​     wrapping activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false

我知道我参加聚会迟到了,但我可以看到你经常使用lifecycleScope.launch,而且在里面你正在调用适配器来submitData。这意味着此适配器将无法正确地进行垃圾收集。这可能是内存泄漏的根源。请尝试使用viewLifecycleOwner.lifecycleScope.launch

这是一个已知的错误:https://proandroiddev.com/5-common-mistakes-when-using-architecture-components-403e9899f4cb

好的,我已经设法解决了这个泄漏。导致泄漏的原因是,我已经通过构造函数注入将ShopAdapter注入到Fragment中。当通过构造函数注入将某些内容注入fragment时,必须将依赖关系传递给MainFragmentFactory。但正因为如此,MainFragmentFactory始终保持对适配器的引用,即使片段被销毁并且不再需要该片段(因此,requireView().findViewById<RecyclerView>(R.id.rv_shop).adapter = null甚至不会在此处进行更改(。

为了解决这个问题,不要通过构造函数注入来注入适配器,而是通过字段注入来注入。

最新更新