Fix possible RxStore memory leak.

This commit is contained in:
Alex Hart
2022-10-05 12:06:47 -03:00
committed by GitHub
parent 4f3910e3ae
commit ee00e931eb
21 changed files with 63 additions and 13 deletions

View File

@@ -70,6 +70,7 @@ class ViewReceivedGiftViewModel(
override fun onCleared() { override fun onCleared() {
disposables.dispose() disposables.dispose()
store.dispose()
} }
fun setChecked(isChecked: Boolean) { fun setChecked(isChecked: Boolean) {

View File

@@ -38,6 +38,7 @@ class ViewSentGiftViewModel(
override fun onCleared() { override fun onCleared() {
disposables.dispose() disposables.dispose()
store.dispose()
} }
class Factory( class Factory(

View File

@@ -62,6 +62,7 @@ class DonorErrorConfigurationViewModel : ViewModel() {
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
store.dispose()
} }
fun setSelectedBadge(badgeIndex: Int) { fun setSelectedBadge(badgeIndex: Int) {

View File

@@ -35,6 +35,7 @@ class ContactChipViewModel : ViewModel() {
disposables.clear() disposables.clear()
disposableMap.values.forEach { it.dispose() } disposableMap.values.forEach { it.dispose() }
disposableMap.clear() disposableMap.clear()
store.dispose()
} }
fun add(selectedContact: SelectedContact) { fun add(selectedContact: SelectedContact) {

View File

@@ -59,6 +59,7 @@ import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.processors.PublishProcessor; import io.reactivex.rxjava3.processors.PublishProcessor;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.BehaviorSubject; import io.reactivex.rxjava3.subjects.BehaviorSubject;
@@ -135,10 +136,12 @@ public class ConversationViewModel extends ViewModel {
.map(Recipient::resolved) .map(Recipient::resolved)
.subscribe(recipientCache); .subscribe(recipientCache);
conversationStateStore.update(Observable.combineLatest(recipientId.distinctUntilChanged(), conversationStateTick, (id, tick) -> id) Disposable disposable = conversationStateStore.update(Observable.combineLatest(recipientId.distinctUntilChanged(), conversationStateTick, (id, tick) -> id)
.switchMap(conversationRepository::getSecurityInfo) .switchMap(conversationRepository::getSecurityInfo)
.toFlowable(BackpressureStrategy.LATEST), .toFlowable(BackpressureStrategy.LATEST),
(securityInfo, state) -> state.withSecurityInfo(securityInfo)); (securityInfo, state) -> state.withSecurityInfo(securityInfo));
disposables.add(disposable);
BehaviorSubject<ConversationData> conversationMetadata = BehaviorSubject.create(); BehaviorSubject<ConversationData> conversationMetadata = BehaviorSubject.create();
@@ -435,6 +438,7 @@ public class ConversationViewModel extends ViewModel {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver); ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver); ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);
disposables.clear(); disposables.clear();
conversationStateStore.dispose();
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
} }

View File

@@ -33,6 +33,10 @@ class DraftViewModel @JvmOverloads constructor(
val voiceNoteDraft: Draft? val voiceNoteDraft: Draft?
get() = store.state.voiceNoteDraft get() = store.state.voiceNoteDraft
override fun onCleared() {
store.dispose()
}
fun setThreadId(threadId: Long) { fun setThreadId(threadId: Long) {
store.update { it.copy(threadId = threadId) } store.update { it.copy(threadId = threadId) }
} }

View File

@@ -41,5 +41,6 @@ class MediaPreviewV2ViewModel : ViewModel() {
override fun onCleared() { override fun onCleared() {
disposables.dispose() disposables.dispose()
store.dispose()
} }
} }

View File

@@ -26,6 +26,10 @@ class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : Vi
} }
} }
override fun onCleared() {
store.dispose()
}
fun onImageCaptured(data: ByteArray, width: Int, height: Int) { fun onImageCaptured(data: ByteArray, width: Int, height: Int) {
repository.renderImageToMedia(data, width, height, this::onMediaRendered, this::onMediaRenderFailed) repository.renderImageToMedia(data, width, height, this::onMediaRendered, this::onMediaRenderFailed)
} }

View File

@@ -29,5 +29,6 @@ class WhoCanSeeMyPhoneNumberViewModel : ViewModel() {
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
store.dispose()
} }
} }

View File

@@ -65,6 +65,7 @@ class UsernameEditViewModel extends ViewModel {
protected void onCleared() { protected void onCleared() {
super.onCleared(); super.onCleared();
disposables.clear(); disposables.clear();
uiState.dispose();
} }
void onNicknameUpdated(@NonNull String nickname) { void onNicknameUpdated(@NonNull String nickname) {

View File

@@ -66,6 +66,8 @@ class SafetyNumberBottomSheetViewModel(
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
destinationStore.dispose()
store.dispose()
} }
fun getIdentityRecord(recipientId: RecipientId): Maybe<IdentityRecord> { fun getIdentityRecord(recipientId: RecipientId): Maybe<IdentityRecord> {

View File

@@ -72,6 +72,7 @@ class ShareViewModel(
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
store.dispose()
} }
private fun moveToFailedState(throwable: Throwable? = null) { private fun moveToFailedState(throwable: Throwable? = null) {

View File

@@ -31,6 +31,7 @@ class ChooseInitialMyStoryMembershipViewModel @JvmOverloads constructor(
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
store.dispose()
} }
fun select(selection: DistributionListPrivacyMode): Single<DistributionListPrivacyMode> { fun select(selection: DistributionListPrivacyMode): Single<DistributionListPrivacyMode> {

View File

@@ -59,13 +59,14 @@ class StoriesPrivacySettingsViewModel : ViewModel() {
pagingController.set(observablePagedData.controller) pagingController.set(observablePagedData.controller)
store.update(observablePagedData.data.toFlowable(BackpressureStrategy.LATEST)) { data, state -> disposables += store.update(observablePagedData.data.toFlowable(BackpressureStrategy.LATEST)) { data, state ->
state.copy(storyContactItems = data) state.copy(storyContactItems = data)
} }
} }
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
store.dispose()
} }
fun setStoriesEnabled(isEnabled: Boolean) { fun setStoriesEnabled(isEnabled: Boolean) {

View File

@@ -143,6 +143,7 @@ class StoryViewerViewModel(
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
store.dispose()
} }
fun setSelectedPage(page: Int) { fun setSelectedPage(page: Int) {

View File

@@ -10,6 +10,10 @@ class StoryVolumeViewModel : ViewModel() {
val state: Flowable<StoryVolumeState> = store.stateFlowable val state: Flowable<StoryVolumeState> = store.stateFlowable
val snapshot: StoryVolumeState get() = store.state val snapshot: StoryVolumeState get() = store.state
override fun onCleared() {
store.dispose()
}
fun mute() { fun mute() {
store.update { it.copy(isMuted = true) } store.update { it.copy(isMuted = true) }
} }

View File

@@ -75,6 +75,7 @@ class StoryInfoViewModel(storyId: Long, repository: StoryInfoRepository = StoryI
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
store.dispose()
} }
class Factory(private val storyId: Long) : ViewModelProvider.Factory { class Factory(private val storyId: Long) : ViewModelProvider.Factory {

View File

@@ -99,6 +99,7 @@ class StoryViewerPageViewModel(
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
storyCache.clear() storyCache.clear()
store.dispose()
} }
fun hideStory(): Completable { fun hideStory(): Completable {

View File

@@ -51,6 +51,7 @@ class StoryGroupReplyViewModel(storyId: Long, repository: StoryGroupReplyReposit
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
store.dispose()
} }
class Factory(private val storyId: Long, private val repository: StoryGroupReplyRepository) : ViewModelProvider.Factory { class Factory(private val storyId: Long, private val repository: StoryGroupReplyRepository) : ViewModelProvider.Factory {

View File

@@ -1,8 +1,10 @@
package org.thoughtcrime.securesms.util.rx package org.thoughtcrime.securesms.util.rx
import androidx.annotation.CheckResult
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Scheduler import io.reactivex.rxjava3.core.Scheduler
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.PublishSubject
@@ -10,11 +12,14 @@ import io.reactivex.rxjava3.subjects.PublishSubject
/** /**
* Rx replacement for Store. * Rx replacement for Store.
* Actions are run on the computation thread by default. * Actions are run on the computation thread by default.
*
* This class is disposable, and should be explicitly disposed of in a ViewModel's onCleared method
* to prevent memory leaks. Disposing instances of this class is a terminal action.
*/ */
class RxStore<T : Any>( class RxStore<T : Any>(
defaultValue: T, defaultValue: T,
scheduler: Scheduler = Schedulers.computation() scheduler: Scheduler = Schedulers.computation()
) { ) : Disposable {
private val behaviorProcessor = BehaviorProcessor.createDefault(defaultValue) private val behaviorProcessor = BehaviorProcessor.createDefault(defaultValue)
private val actionSubject = PublishSubject.create<(T) -> T>().toSerialized() private val actionSubject = PublishSubject.create<(T) -> T>().toSerialized()
@@ -22,20 +27,30 @@ class RxStore<T : Any>(
val state: T get() = behaviorProcessor.value!! val state: T get() = behaviorProcessor.value!!
val stateFlowable: Flowable<T> = behaviorProcessor.onBackpressureLatest() val stateFlowable: Flowable<T> = behaviorProcessor.onBackpressureLatest()
init { val actionDisposable: Disposable = actionSubject
actionSubject .observeOn(scheduler)
.observeOn(scheduler) .scan(defaultValue) { v, f -> f(v) }
.scan(defaultValue) { v, f -> f(v) } .subscribe { behaviorProcessor.onNext(it) }
.subscribe { behaviorProcessor.onNext(it) }
}
fun update(transformer: (T) -> T) { fun update(transformer: (T) -> T) {
actionSubject.onNext(transformer) actionSubject.onNext(transformer)
} }
fun <U> update(flowable: Flowable<U>, transformer: (U, T) -> T): Disposable { @CheckResult
fun <U : Any> update(flowable: Flowable<U>, transformer: (U, T) -> T): Disposable {
return flowable.subscribe { return flowable.subscribe {
actionSubject.onNext { t -> transformer(it, t) } actionSubject.onNext { t -> transformer(it, t) }
} }
} }
/**
* Dispose of the underlying scan chain. This is terminal.
*/
override fun dispose() {
actionDisposable.dispose()
}
override fun isDisposed(): Boolean {
return actionDisposable.isDisposed
}
} }

View File

@@ -33,6 +33,7 @@ class RxStoreTest {
// THEN // THEN
subscriber.assertValueAt(0, 1) subscriber.assertValueAt(0, 1)
subscriber.assertNotComplete() subscriber.assertNotComplete()
testSubject.dispose()
} }
@Test @Test
@@ -50,6 +51,7 @@ class RxStoreTest {
subscriber.assertValueAt(0, 1) subscriber.assertValueAt(0, 1)
subscriber.assertValueAt(1, 2) subscriber.assertValueAt(1, 2)
subscriber.assertNotComplete() subscriber.assertNotComplete()
testSubject.dispose()
} }
@Test @Test
@@ -66,5 +68,6 @@ class RxStoreTest {
// THEN // THEN
subscriber.assertValueAt(0, 2) subscriber.assertValueAt(0, 2)
subscriber.assertNotComplete() subscriber.assertNotComplete()
testSubject.dispose()
} }
} }