【问题标题】:Android: ViewModel with paging 3 flow is leakingAndroid:具有分页 3 流的 ViewModel 正在泄漏
【发布时间】:2021-01-29 07:19:59
【问题描述】:

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

视图模型

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)
    }
}

MainFragmentFactory

​​>
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

【问题讨论】:

  • 那么MainFragmentFactory.shopAdapter 是什么?在onDestroyView() 之后您无法握住适配器 - 这就是导致泄漏的原因。
  • @ianhanniballake 我编辑了我的帖子并添加了我的shopAdaterFragmentFactory。我是否必须将我的适配器范围限定为@FragmentScoped?奇怪的是,我在三个不同的片段中使用了这种方法,而且只有一个有分页源泄漏(shopviewmodel)。
  • 或者可能与我分配clickListener 的方式有关?我正在写shopAdater.clickHandler(this)。这不是意味着我将片段引用传递给我的适配器吗?

标签: android kotlin memory-leaks android-mvvm android-paging


【解决方案1】:

好的,我已经设法解决了这个漏洞。导致泄漏是因为我通过构造函数注入将我的ShopAdapter 注入到我的Fragment 中。当通过构造函数注入fragment注入一些东西时,你必须将依赖传递给MainFragmentFactory。但正因为如此,MainFragmentFactory始终持有对适配器的引用,即使片段被销毁并且不再需要片段(因此,requireView().findViewById&lt;RecyclerView&gt;(R.id.rv_shop).adapter = null 甚至不会这里有一个变化)。

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

【讨论】:

    【解决方案2】:

    我知道我迟到了,但我可以看到您经常使用lifecycleScope.launch,并且在内部您正在调用适配器来提交数据。这意味着该适配器将无法正确地进行垃圾收集。这可能是内存泄漏的根源。 尝试改用viewLifecycleOwner.lifecycleScope.launch

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-12-06
      • 1970-01-01
      • 1970-01-01
      • 2012-04-07
      • 2011-12-23
      • 2017-06-18
      • 2013-12-25
      • 2014-08-19
      相关资源
      最近更新 更多