Skip to content

Commit c515971

Browse files
committed
Fixes memory leak in ViewStateCache.
`ViewStateCache` was `Parcelable`, in an attempt to simplify `View.onSaveInstanceState()` chores for client container views. That became a source of memory leaks once we added a `WorkflowSavedStateRegistryAggregator` to it, so we've dialed back the convenience. `ViewStateCache` now offers `save(): Saved` and `restore(saved: Saved)`, where `Saved` is a simple `Parcelable`. Client views are responsible for managing their own subclasses of `View.BaseSavedState`, which is sad but not terribly unusual.
1 parent 64e3073 commit c515971

File tree

4 files changed

+102
-87
lines changed

4 files changed

+102
-87
lines changed

workflow-ui/container-android/api/container-android.api

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,39 +33,46 @@ public final class com/squareup/workflow1/ui/backstack/BackStackContainer$Compan
3333
public fun getType ()Lkotlin/reflect/KClass;
3434
}
3535

36-
public final class com/squareup/workflow1/ui/backstack/ViewStateCache : android/os/Parcelable {
37-
public static final field CREATOR Lcom/squareup/workflow1/ui/backstack/ViewStateCache$CREATOR;
36+
public final class com/squareup/workflow1/ui/backstack/BackStackContainer$SavedState : android/view/View$BaseSavedState {
37+
public static final field CREATOR Lcom/squareup/workflow1/ui/backstack/BackStackContainer$SavedState$CREATOR;
38+
public fun <init> (Landroid/os/Parcel;)V
39+
public fun <init> (Landroid/os/Parcelable;Lcom/squareup/workflow1/ui/backstack/ViewStateCache$Saved;)V
40+
public final fun getSavedViewState ()Lcom/squareup/workflow1/ui/backstack/ViewStateCache$Saved;
41+
public fun writeToParcel (Landroid/os/Parcel;I)V
42+
}
43+
44+
public final class com/squareup/workflow1/ui/backstack/BackStackContainer$SavedState$CREATOR : android/os/Parcelable$Creator {
45+
public fun createFromParcel (Landroid/os/Parcel;)Lcom/squareup/workflow1/ui/backstack/ViewStateCache$Saved;
46+
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
47+
public fun newArray (I)[Lcom/squareup/workflow1/ui/backstack/ViewStateCache$Saved;
48+
public synthetic fun newArray (I)[Ljava/lang/Object;
49+
}
50+
51+
public final class com/squareup/workflow1/ui/backstack/ViewStateCache {
3852
public fun <init> ()V
3953
public fun <init> (Ljava/util/Map;)V
4054
public final fun attachToParentRegistryOwner (Ljava/lang/String;Landroidx/savedstate/SavedStateRegistryOwner;)V
41-
public fun describeContents ()I
4255
public final fun detachFromParentRegistry ()V
4356
public final fun getViewStates$wf1_container_android ()Ljava/util/Map;
4457
public final fun prune (Ljava/util/Collection;)V
45-
public final fun restore (Lcom/squareup/workflow1/ui/backstack/ViewStateCache;)V
58+
public final fun restore (Lcom/squareup/workflow1/ui/backstack/ViewStateCache$Saved;)V
59+
public final fun save ()Lcom/squareup/workflow1/ui/backstack/ViewStateCache$Saved;
4660
public final fun update (Ljava/util/Collection;Landroid/view/View;Landroid/view/View;)V
47-
public fun writeToParcel (Landroid/os/Parcel;I)V
48-
}
49-
50-
public final class com/squareup/workflow1/ui/backstack/ViewStateCache$CREATOR : android/os/Parcelable$Creator {
51-
public fun createFromParcel (Landroid/os/Parcel;)Lcom/squareup/workflow1/ui/backstack/ViewStateCache;
52-
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
53-
public fun newArray (I)[Lcom/squareup/workflow1/ui/backstack/ViewStateCache;
54-
public synthetic fun newArray (I)[Ljava/lang/Object;
5561
}
5662

57-
public final class com/squareup/workflow1/ui/backstack/ViewStateCache$SavedState : android/view/View$BaseSavedState {
58-
public static final field CREATOR Lcom/squareup/workflow1/ui/backstack/ViewStateCache$SavedState$CREATOR;
63+
public final class com/squareup/workflow1/ui/backstack/ViewStateCache$Saved : android/os/Parcelable {
64+
public static final field CREATOR Lcom/squareup/workflow1/ui/backstack/ViewStateCache$Saved$CREATOR;
5965
public fun <init> (Landroid/os/Parcel;)V
60-
public fun <init> (Landroid/os/Parcelable;Lcom/squareup/workflow1/ui/backstack/ViewStateCache;)V
61-
public final fun getViewStateCache ()Lcom/squareup/workflow1/ui/backstack/ViewStateCache;
66+
public fun <init> (Lcom/squareup/workflow1/ui/backstack/ViewStateCache;)V
67+
public fun describeContents ()I
68+
public final fun getViewStates$wf1_container_android ()Ljava/util/Map;
6269
public fun writeToParcel (Landroid/os/Parcel;I)V
6370
}
6471

65-
public final class com/squareup/workflow1/ui/backstack/ViewStateCache$SavedState$CREATOR : android/os/Parcelable$Creator {
66-
public fun createFromParcel (Landroid/os/Parcel;)Lcom/squareup/workflow1/ui/backstack/ViewStateCache$SavedState;
72+
public final class com/squareup/workflow1/ui/backstack/ViewStateCache$Saved$CREATOR : android/os/Parcelable$Creator {
73+
public fun createFromParcel (Landroid/os/Parcel;)Lcom/squareup/workflow1/ui/backstack/ViewStateCache$Saved;
6774
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
68-
public fun newArray (I)[Lcom/squareup/workflow1/ui/backstack/ViewStateCache$SavedState;
75+
public fun newArray (I)[Lcom/squareup/workflow1/ui/backstack/ViewStateCache$Saved;
6976
public synthetic fun newArray (I)[Ljava/lang/Object;
7077
}
7178

workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/ViewStateCacheTest.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ internal class ViewStateCacheTest {
4343
)
4444
val parcel = Parcel.obtain()
4545

46-
parcel.writeParcelable(cache, 0)
46+
parcel.writeParcelable(cache.save(), 0)
4747

4848
parcel.setDataPosition(0)
49-
val restoredCache =
50-
parcel.readParcelable<ViewStateCache>(ViewStateCache::class.java.classLoader)!!.also {
51-
ViewStateCache().restore(it)
52-
}
49+
val restoredCache = parcel.readParcelable<ViewStateCache.Saved>(
50+
ViewStateCache.Saved::class.java.classLoader
51+
)!!.let { restoredState ->
52+
ViewStateCache().apply { restore(restoredState) }
53+
}
5354

5455
assertThat(restoredCache.equalsForTest(cache)).isTrue()
5556
}

workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/BackStackContainer.kt

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.squareup.workflow1.ui.backstack
22

33
import android.content.Context
4+
import android.os.Parcel
45
import android.os.Parcelable
6+
import android.os.Parcelable.Creator
57
import android.util.AttributeSet
68
import android.view.Gravity
79
import android.view.View
@@ -143,14 +145,16 @@ public open class BackStackContainer @JvmOverloads constructor(
143145
addView(newView)
144146
}
145147

146-
override fun onSaveInstanceState(): Parcelable {
147-
return ViewStateCache.SavedState(super.onSaveInstanceState(), viewStateCache)
148+
override fun onSaveInstanceState(): Parcelable? {
149+
return super.onSaveInstanceState()?.let {
150+
SavedState(it, viewStateCache.save())
151+
}
148152
}
149153

150154
override fun onRestoreInstanceState(state: Parcelable) {
151-
(state as? ViewStateCache.SavedState)
155+
(state as? SavedState)
152156
?.let {
153-
viewStateCache.restore(it.viewStateCache)
157+
viewStateCache.restore(it.savedViewState)
154158
super.onRestoreInstanceState(state.superState)
155159
}
156160
?: super.onRestoreInstanceState(super.onSaveInstanceState())
@@ -174,6 +178,36 @@ public open class BackStackContainer @JvmOverloads constructor(
174178
super.onDetachedFromWindow()
175179
}
176180

181+
public class SavedState : BaseSavedState {
182+
public constructor(
183+
superState: Parcelable,
184+
savedViewState: ViewStateCache.Saved
185+
) : super(superState) {
186+
this.savedViewState = savedViewState
187+
}
188+
189+
public constructor(source: Parcel) : super(source) {
190+
this.savedViewState = source.readParcelable(ViewStateCache.Saved::class.java.classLoader)!!
191+
}
192+
193+
public val savedViewState: ViewStateCache.Saved
194+
195+
override fun writeToParcel(
196+
out: Parcel,
197+
flags: Int
198+
) {
199+
super.writeToParcel(out, flags)
200+
out.writeParcelable(savedViewState, flags)
201+
}
202+
203+
public companion object CREATOR : Creator<ViewStateCache.Saved> {
204+
override fun createFromParcel(source: Parcel): ViewStateCache.Saved =
205+
ViewStateCache.Saved(source)
206+
207+
override fun newArray(size: Int): Array<ViewStateCache.Saved?> = arrayOfNulls(size)
208+
}
209+
}
210+
177211
public companion object : ViewFactory<BackStackScreen<*>>
178212
by BuilderViewFactory(
179213
type = BackStackScreen::class,

workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/backstack/ViewStateCache.kt

Lines changed: 32 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,22 @@ import android.os.Parcelable
55
import android.os.Parcelable.Creator
66
import android.util.SparseArray
77
import android.view.View
8-
import android.view.View.BaseSavedState
98
import androidx.annotation.VisibleForTesting
109
import androidx.annotation.VisibleForTesting.PRIVATE
1110
import androidx.savedstate.SavedStateRegistryOwner
1211
import androidx.savedstate.ViewTreeSavedStateRegistryOwner
1312
import com.squareup.workflow1.ui.Named
1413
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
1514
import com.squareup.workflow1.ui.androidx.WorkflowSavedStateRegistryAggregator
16-
import com.squareup.workflow1.ui.backstack.ViewStateCache.SavedState
1715
import com.squareup.workflow1.ui.getRendering
1816

1917
/**
2018
* Handles persistence chores for container views that manage a set of [Named] renderings,
2119
* showing a view for one at a time -- think back stacks or tab sets.
2220
*
23-
* - This class implements [Parcelable] so that it can be preserved from
24-
* a container view's own [View.saveHierarchyState] method. A simple container can
25-
* return [SavedState] from that method rather than creating its own persistence class.
26-
*
27-
* - It also handles androidx [ViewTreeSavedStateRegistryOwner] duties, via
21+
* - Provides [Parcelable]-based [save] and [restore] methods for use from a
22+
* container's [View.onSaveInstanceState] and [View.onRestoreInstanceState] methods.
23+
* - Also handles androidx [ViewTreeSavedStateRegistryOwner] duties, via
2824
* a wrapped instance of [WorkflowSavedStateRegistryAggregator]. This means that container
2925
* views using this class must call [attachToParentRegistryOwner] and
3026
* [detachFromParentRegistry] when they are [attached][View.onAttachedToWindow] and
@@ -36,7 +32,7 @@ public class ViewStateCache
3632
internal constructor(
3733
@VisibleForTesting(otherwise = PRIVATE)
3834
internal val viewStates: MutableMap<String, ViewStateFrame>
39-
) : Parcelable {
35+
) {
4036
public constructor() : this(mutableMapOf())
4137

4238
private val stateRegistryAggregator = WorkflowSavedStateRegistryAggregator()
@@ -134,77 +130,54 @@ internal constructor(
134130
* Replaces the state of the receiver with that of [from]. Typical usage is to call this from
135131
* a container view's [View.onRestoreInstanceState].
136132
*/
137-
public fun restore(from: ViewStateCache) {
133+
public fun restore(from: Saved) {
138134
viewStates.clear()
139135
viewStates += from.viewStates
140136
}
141137

142138
/**
143-
* Convenience for use in [View.onSaveInstanceState] and [View.onRestoreInstanceState]
144-
* methods of container views that have no other state of their own to save.
145-
*
146-
* More interesting containers should create their own subclass of [BaseSavedState]
147-
* rather than trying to extend this one.
139+
* Returns a [Parcelable] copy of the internal state of the receiver, for use with
140+
* a container view's [View.onSaveInstanceState].
148141
*/
149-
public class SavedState : BaseSavedState {
150-
public constructor(
151-
superState: Parcelable?,
152-
viewStateCache: ViewStateCache
153-
) : super(superState) {
154-
this.viewStateCache = viewStateCache
142+
public fun save(): Saved {
143+
return Saved(this)
144+
}
145+
146+
public class Saved : Parcelable {
147+
internal constructor(viewStateCache: ViewStateCache) {
148+
this.viewStates = viewStateCache.viewStates.toMap()
155149
}
156150

157-
public constructor(source: Parcel) : super(source) {
158-
this.viewStateCache = source.readParcelable(SavedState::class.java.classLoader)!!
151+
public constructor(source: Parcel) {
152+
this.viewStates = mutableMapOf<String, ViewStateFrame>()
153+
.apply {
154+
@Suppress("UNCHECKED_CAST")
155+
source.readMap(
156+
this as MutableMap<Any?, Any?>,
157+
ViewStateCache::class.java.classLoader
158+
)
159+
}
160+
.toMap()
159161
}
160162

161-
public val viewStateCache: ViewStateCache
163+
internal val viewStates: Map<String, ViewStateFrame>
164+
165+
override fun describeContents(): Int = 0
162166

163167
override fun writeToParcel(
164168
out: Parcel,
165169
flags: Int
166170
) {
167-
super.writeToParcel(out, flags)
168-
out.writeParcelable(viewStateCache, flags)
171+
out.writeMap(viewStates)
169172
}
170173

171-
public companion object CREATOR : Creator<SavedState> {
172-
override fun createFromParcel(source: Parcel): SavedState =
173-
SavedState(source)
174+
public companion object CREATOR : Creator<Saved> {
175+
override fun createFromParcel(source: Parcel): Saved =
176+
Saved(source)
174177

175-
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
178+
override fun newArray(size: Int): Array<Saved?> = arrayOfNulls(size)
176179
}
177180
}
178-
179-
// region Parcelable
180-
181-
override fun describeContents(): Int = 0
182-
183-
override fun writeToParcel(
184-
parcel: Parcel,
185-
flags: Int
186-
) {
187-
@Suppress("UNCHECKED_CAST")
188-
parcel.writeMap(viewStates as MutableMap<Any?, Any?>)
189-
}
190-
191-
public companion object CREATOR : Creator<ViewStateCache> {
192-
override fun createFromParcel(parcel: Parcel): ViewStateCache {
193-
@Suppress("UNCHECKED_CAST")
194-
return mutableMapOf<String, ViewStateFrame>()
195-
.apply {
196-
parcel.readMap(
197-
this as MutableMap<Any?, Any?>,
198-
ViewStateCache::class.java.classLoader
199-
)
200-
}
201-
.let { ViewStateCache(it) }
202-
}
203-
204-
override fun newArray(size: Int): Array<ViewStateCache?> = arrayOfNulls(size)
205-
}
206-
207-
// endregion
208181
}
209182

210183
@WorkflowUiExperimentalApi

0 commit comments

Comments
 (0)