Android 分页流动泄漏视图模型和片段

Android paging flowable leaking viewmodel and fragment

我正在使用带有 RxJava 源代码的分页 3 android 库。我有两个片段,第一个在网格中显示图像列表,单击图像时显示第二个片段,它全屏显示图像,并有一个 ViewPager 在图像之间滑动。因为那些使用相同的数据我想我可以使用共享视图模型,在我有的两个片段中

@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by activityViewModels<FilesViewModel> { viewModelFactory }

并且视图模型创建了两个片段在其视图可见时观察到的 rx 可流动

class FilesViewModel @Inject constructor(
    settings: SettingsRepository,
    private val filesRepository: FilesRepository
): ViewModel() {

    ...

    var cachedFileList = filesRepository.getPagedFiles("path").cachedIn(viewModelScope)

    ...

} 

导航回列表后,片段被保留,这样做五次后,LeakCanary 显示泄漏

┬───
│ GC Root: System class
│
├─ leakcanary.internal.InternalLeakCanary class
│    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│    ↓ static InternalLeakCanary.resumedActivity
├─ com.guillermonegrete.gallery.folders.MainActivity instance
│    Leaking: NO (Activity#mDestroyed is false)
│    mApplication instance of com.guillermonegrete.gallery.MyApplication
│    mBase instance of android.app.ContextImpl
│    ↓ ComponentActivity.mViewModelStore
│                        ~~~~~~~~~~~~~~~
├─ androidx.lifecycle.ViewModelStore instance
│    Leaking: UNKNOWN
│    Retaining 816 B in 11 objects
│    ↓ ViewModelStore.mMap
│                     ~~~~
├─ java.util.HashMap instance
│    Leaking: UNKNOWN
│    Retaining 804 B in 10 objects
│    ↓ HashMap.table
│              ~~~~~
├─ java.util.HashMap$HashMapEntry[] array
│    Leaking: UNKNOWN
│    Retaining 764 B in 9 objects
│    ↓ HashMap$HashMapEntry[].[0]
│                             ~~~
├─ java.util.HashMap$HashMapEntry instance
│    Leaking: UNKNOWN
│    Retaining 520 B in 6 objects
│    ↓ HashMap$HashMapEntry.value
│                           ~~~~~
├─ com.guillermonegrete.gallery.files.FilesViewModel instance
│    Leaking: UNKNOWN
│    Retaining 4,0 kB in 145 objects
│    ↓ FilesViewModel.cachedFileList
│                     ~~~~~~~~~~~~~~
├─ io.reactivex.internal.operators.flowable.FlowableFromPublisher instance
│    Leaking: UNKNOWN
│    Retaining 28 B in 2 objects
│    ↓ FlowableFromPublisher.publisher
│                            ~~~~~~~~~
├─ kotlinx.coroutines.reactive.FlowAsPublisher instance
│    Leaking: UNKNOWN
│    Retaining 16 B in 1 objects
│    ↓ FlowAsPublisher.flow
│                      ~~~~
├─ kotlinx.coroutines.flow.SafeFlow instance
│    Leaking: UNKNOWN
│    Retaining 48 B in 2 objects
│    ↓ SafeFlow.block
│               ~~~~~
├─ androidx.paging.multicast.Multicaster$flow instance
│    Leaking: UNKNOWN
│    Retaining 36 B in 1 objects
│    Anonymous subclass of kotlin.coroutines.jvm.internal.SuspendLambda
│    ↓ Multicaster$flow.this[=13=]
│                         ~~~~~~
├─ androidx.paging.multicast.Multicaster instance
│    Leaking: UNKNOWN
│    Retaining 108 B in 4 objects
│    ↓ Multicaster.channelManager$delegate
│                  ~~~~~~~~~~~~~~~~~~~~~~~
├─ kotlin.SynchronizedLazyImpl instance
│    Leaking: UNKNOWN
│    Retaining 50 B in 2 objects
│    ↓ SynchronizedLazyImpl._value
│                           ~~~~~~
├─ androidx.paging.multicast.ChannelManager instance
│    Leaking: UNKNOWN
│    Retaining 30 B in 1 objects
│    ↓ ChannelManager.actor
│                     ~~~~~
├─ androidx.paging.multicast.ChannelManager$Actor instance
│    Leaking: UNKNOWN
│    Retaining 19,5 kB in 573 objects
│    ↓ ChannelManager$Actor.channels
│                           ~~~~~~~~
├─ java.util.ArrayList instance
│    Leaking: UNKNOWN
│    Retaining 19,4 kB in 567 objects
│    ↓ ArrayList.elementData
│                ~~~~~~~~~~~
├─ java.lang.Object[] array
│    Leaking: UNKNOWN
│    Retaining 19,4 kB in 566 objects
│    ↓ Object[].[1]
│               ~~~
├─ androidx.paging.multicast.ChannelManager$ChannelEntry instance
│    Leaking: UNKNOWN
│    Retaining 3,6 kB in 110 objects
│    ↓ ChannelManager$ChannelEntry.channel
│                                  ~~~~~~~
├─ kotlinx.coroutines.channels.LinkedListChannel instance
│    Leaking: UNKNOWN
│    Retaining 3,6 kB in 109 objects
│    ↓ AbstractSendChannel.queue
│                          ~~~~~
├─ kotlinx.coroutines.internal.LockFreeLinkedListHead instance
│    Leaking: UNKNOWN
│    Retaining 3,6 kB in 108 objects
│    ↓ LockFreeLinkedListNode._next
│                             ~~~~~
├─ kotlinx.coroutines.channels.Closed instance
│    Leaking: UNKNOWN
│    Retaining 3,6 kB in 106 objects
│    ↓ Closed.closeCause
│             ~~~~~~~~~~
├─ kotlinx.coroutines.JobCancellationException instance
│    Leaking: UNKNOWN
│    Retaining 3,5 kB in 105 objects
│    ↓ JobCancellationException.job
│                               ~~~
├─ kotlinx.coroutines.reactive.FlowSubscription instance
│    Leaking: UNKNOWN
│    Retaining 3,4 kB in 102 objects
│    ↓ FlowSubscription.subscriber
│                       ~~~~~~~~~~
├─ io.reactivex.internal.operators.flowable.
│  FlowableSubscribeOn$SubscribeOnSubscriber instance
│    Leaking: UNKNOWN
│    Retaining 3,3 kB in 99 objects
│    ↓ FlowableSubscribeOn$SubscribeOnSubscriber.downstream
│                                                ~~~~~~~~~~
├─ io.reactivex.internal.operators.flowable.
│  FlowableObserveOn$ObserveOnSubscriber instance
│    Leaking: UNKNOWN
│    Retaining 3,2 kB in 93 objects
│    ↓ FlowableObserveOn$ObserveOnSubscriber.downstream
│                                            ~~~~~~~~~~
├─ io.reactivex.internal.subscribers.LambdaSubscriber instance
│    Leaking: UNKNOWN
│    Retaining 2,6 kB in 86 objects
│    ↓ LambdaSubscriber.onNext
│                       ~~~~~~
├─ com.guillermonegrete.gallery.files.details.
│  FileDetailsFragment$setUpViewModel instance
│    Leaking: UNKNOWN
│    Retaining 2,5 kB in 85 objects
│    Anonymous class implementing io.reactivex.functions.Consumer
│    ↓ FileDetailsFragment$setUpViewModel.this[=13=]
│                                           ~~~~~~
╰→ com.guillermonegrete.gallery.files.details.FileDetailsFragment instance
     Leaking: YES (ObjectWatcher was watching this because com.
     guillermonegrete.gallery.files.details.FileDetailsFragment received
     Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
     Retaining 2,5 kB in 84 objects
     key = 0b0dad5d-1c55-4938-94d8-0f923fc29508
     watchDurationMillis = 23025
     retainedDurationMillis = 18023

可以看到第二个片段泄露了,跟cachedFileList有关 现在,如果我删除 cachedIn(viewModelScope) 那么泄漏就消失了,但是应用程序现在每次在片段之间导航时都会进行 API 调用,共享视图模型的全部意义在于保存 api来电。

有什么方法可以避免多次 api 调用和泄漏?我知道我可以使用数据库,但我想尽可能避免这种开销。

编辑:

流量是怎么消耗的,两个分片基本一样

class FilesListFragment: Fragment(R.layout.fragment_files_list) {
   ...
   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        bindViewModel(folder)
   }
   
   private fun bindViewModel(folder: String){
       disposable.add(viewModel.loadPagedFiles(folder)
            .subscribeOn(Schedulers.io()) // Omitted some mapping
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                { adapter.submitData(lifecycle, it) },
                { error -> println("Error loading files: ${error.message}") }
            )
        )
   }

    override fun onDestroyView() {
        binding.filesList.adapter = null
        _binding = null
        disposable.clear()
        adapter.removeLoadStateListener(loadListener)
        super.onDestroyView()
    }
   ...
}

这是它的创建方式

class DefaultFilesRepository @Inject constructor(private var fileAPI: FilesServerAPI): FilesRepository {

    override fun getPagedFiles(folder: String): Flowable<PagingData<File>> {
        return Pager(PagingConfig(pageSize = 20)) {
            FilesPageSource(fileAPI, baseUrl, folder)
        }.flowable
    }

}

根据@dlam 的推荐,我使用了 switchMap.

class FilesViewModel @Inject constructor(
    settings: SettingsRepository,
    private val filesRepository: FilesRepository
): ViewModel() {

    ...

    private val folderName: Subject<String> = PublishSubject.create()

    var cachedFileList: Flowable<PagingData<File>> = folderName.distinctUntilChanged().switchMap {
        filesRepository.getPagedFiles(it).toObservable()
    }.toFlowable(BackpressureStrategy.LATEST).cachedIn(viewModelScope)

    fun setFolderName(name: String){
        folderName.onNext(name)
    }
    ...
} 

并观察碎片中的数据

disposable.add(viewModel.loadPagedFiles(folder)
            // .subscribeOn(Schedulers.io()) 
            .observeOn(AndroidSchedulers.mainThread()) // Omitted some mapping
            .subscribe(
                { adapter.submitData(lifecycle, it) },
                { error -> println("Error loading files: ${error.message}") }
            )
        )
viewModel.setFolderName(folder)

我从第一个片段中删除了 .subscribeOn(Schedulers.io()),由于某种原因,它导致 switchMap 在开始时永远不会被调用。一个相关的 .

还删除了第二个片段中的那个。导航回第一个片段时抛出此异常:

kotlinx.coroutines.channels.ClosedSendChannelException: Channel was closed

删除 subscribeOn 后异常消失。