Skip to content

Commit

Permalink
Fix memory leaks by releasing references to Paparazzi from Compose ru…
Browse files Browse the repository at this point in the history
…ntime

The Compose runtime via AndroidUiDispatcher and Recomposer holds onto the
Paparazzi instance...and LayoutLib!  As a result, heap space is quickly
clobbered resulting in OOMs.

Related:
https://twitter.com/Piwai/status/1522694063413112832
https://issuetracker.google.com/issues/209497244#comment3
https://android-review.googlesource.com/c/platform/frameworks/support/+/2105346/
  • Loading branch information
jrodbx committed May 26, 2022
1 parent 6d3523e commit b3f745e
Showing 1 changed file with 36 additions and 0 deletions.
36 changes: 36 additions & 0 deletions paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import android.view.ViewGroup.LayoutParams
import android.widget.FrameLayout
import androidx.annotation.LayoutRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Recomposer
import androidx.compose.ui.platform.AndroidUiDispatcher
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
Expand Down Expand Up @@ -76,6 +78,7 @@ import java.lang.reflect.Field
import java.lang.reflect.Modifier
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.coroutines.ContinuationInterceptor

class Paparazzi @JvmOverloads constructor(
private val environment: Environment = detectEnvironment(),
Expand Down Expand Up @@ -193,6 +196,8 @@ class Paparazzi @JvmOverloads constructor(
PaparazziComposeOwner.register(parent)
hostView.setContent(composable)
snapshot(parent, name)

forceReleaseComposeReferenceLeaks()
}

@JvmOverloads
Expand Down Expand Up @@ -539,6 +544,37 @@ class Paparazzi @JvmOverloads constructor(
)
}

private fun forceReleaseComposeReferenceLeaks() {
val snapshotClass = Class.forName("androidx.compose.runtime.snapshots.SnapshotKt")
val applyObservers = snapshotClass
.getDeclaredField("applyObservers")
.apply { isAccessible = true }
.get(null) as MutableList<*>
val applyObserver = applyObservers.getOrNull(0) ?: return
val recomposer = applyObserver.javaClass
.getDeclaredField("this\$0")
.apply { isAccessible = true }
.get(applyObserver) as Recomposer
val compositionInvalidations = recomposer.javaClass
.getDeclaredField("compositionInvalidations")
.apply { isAccessible = true }
.get(recomposer) as MutableList<*>
val snapshotInvalidations = recomposer.javaClass
.getDeclaredField("snapshotInvalidations")
.apply { isAccessible = true }
.get(recomposer) as MutableList<*>
compositionInvalidations.clear()
snapshotInvalidations.clear()
applyObservers.clear()
val dispatcher =
AndroidUiDispatcher.CurrentThread[ContinuationInterceptor] as AndroidUiDispatcher
val toRunTrampolined = dispatcher.javaClass
.getDeclaredField("toRunTrampolined")
.apply { isAccessible = true }
.get(dispatcher) as ArrayDeque<*>
toRunTrampolined.clear()
}

private class PaparazziComposeOwner private constructor() : LifecycleOwner, SavedStateRegistryOwner {
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private val savedStateRegistryController = SavedStateRegistryController.create(this)
Expand Down

0 comments on commit b3f745e

Please sign in to comment.