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 后异常消失。
我正在使用带有 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 后异常消失。