diff --git a/android/build.gradle b/android/build.gradle index 6c88ff8b..a5bd51c6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -47,6 +47,7 @@ android { minSdkVersion getExtOrIntegerDefault("minSdkVersion") targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") buildConfigField "boolean", "RIVE_SKIP_SETUP", "$skipSetup" + buildConfigField "String", "RIVE_ANDROID_VERSION", "\"${getRiveAndroidVersion()}\"" externalNativeBuild { cmake { diff --git a/android/src/legacy/java/com/margelo/nitro/rive/HybridViewModel.kt b/android/src/legacy/java/com/margelo/nitro/rive/HybridViewModel.kt index 9a0253b6..2c42fd46 100644 --- a/android/src/legacy/java/com/margelo/nitro/rive/HybridViewModel.kt +++ b/android/src/legacy/java/com/margelo/nitro/rive/HybridViewModel.kt @@ -22,7 +22,7 @@ class HybridViewModel(private val viewModel: ViewModel) : HybridViewModelSpec() override fun createInstanceByIndex(index: Double): HybridViewModelInstanceSpec? { if (index < 0) return null try { - val vmi = viewModel.createInstanceFromIndex(index.toInt()) + val vmi = viewModel.createInstanceFromIndex(index.toInt()).dropExtraNativeRef() return HybridViewModelInstance(vmi) } catch (e: ViewModelException) { return null @@ -31,7 +31,7 @@ class HybridViewModel(private val viewModel: ViewModel) : HybridViewModelSpec() override fun createInstanceByName(name: String): HybridViewModelInstanceSpec? { try { - val vmi = viewModel.createInstanceFromName(name) + val vmi = viewModel.createInstanceFromName(name).dropExtraNativeRef() return HybridViewModelInstance(vmi) } catch (e: ViewModelException) { return null @@ -40,7 +40,7 @@ class HybridViewModel(private val viewModel: ViewModel) : HybridViewModelSpec() override fun createDefaultInstance(): HybridViewModelInstanceSpec? { try { - val vmi = viewModel.createDefaultInstance() + val vmi = viewModel.createDefaultInstance().dropExtraNativeRef() return HybridViewModelInstance(vmi) } catch (e: ViewModelException) { return null @@ -49,7 +49,7 @@ class HybridViewModel(private val viewModel: ViewModel) : HybridViewModelSpec() override fun createInstance(): HybridViewModelInstanceSpec? { try { - val vmi = viewModel.createBlankInstance() + val vmi = viewModel.createBlankInstance().dropExtraNativeRef() return HybridViewModelInstance(vmi) } catch (e: ViewModelException) { return null diff --git a/android/src/legacy/java/com/margelo/nitro/rive/ViewModelInstanceExtraRef.kt b/android/src/legacy/java/com/margelo/nitro/rive/ViewModelInstanceExtraRef.kt new file mode 100644 index 00000000..6049bade --- /dev/null +++ b/android/src/legacy/java/com/margelo/nitro/rive/ViewModelInstanceExtraRef.kt @@ -0,0 +1,35 @@ +package com.margelo.nitro.rive + +import android.util.Log +import app.rive.runtime.kotlin.core.ViewModelInstance +import com.rive.BuildConfig + +// rive-android's ViewModel.create*Instance hands Kotlin a pointer that already +// owns one native ref, and the ViewModelInstance constructor takes a second that +// release() never drops, so created instances (and images bound to them) outlive +// their File. Dropping it on a release without the bug would free an instance +// still in use, so only versions known to be affected are patched. +private val AFFECTED_VERSIONS = versionCode(10, 4, 5)..versionCode(11, 14, 3) + +private fun versionCode(major: Int, minor: Int, patch: Int) = + major * 1_000_000L + minor * 1_000L + patch + +private fun parseVersion(version: String): Long? { + val parts = version.substringBefore('-').split('.').map { it.toIntOrNull() ?: return null } + if (parts.size != 3) return null + return versionCode(parts[0], parts[1], parts[2]) +} + +private val derefInstance = runCatching { + val version = parseVersion(BuildConfig.RIVE_ANDROID_VERSION) + if (version == null || version !in AFFECTED_VERSIONS) return@runCatching null + ViewModelInstance::class.java + .getDeclaredMethod("cppDerefInstance", Long::class.javaPrimitiveType) + .apply { isAccessible = true } +}.onFailure { Log.w("RiveViewModelInstance", "Can't drop the extra native ref", it) } + .getOrNull() + +internal fun ViewModelInstance.dropExtraNativeRef(): ViewModelInstance { + derefInstance?.invoke(this, cppPointer) + return this +} diff --git a/android/src/legacy/java/com/rive/RiveReactNativeView.kt b/android/src/legacy/java/com/rive/RiveReactNativeView.kt index c07af64e..c435f608 100644 --- a/android/src/legacy/java/com/rive/RiveReactNativeView.kt +++ b/android/src/legacy/java/com/rive/RiveReactNativeView.kt @@ -24,6 +24,7 @@ import app.rive.runtime.kotlin.core.errors.ViewModelException import com.margelo.nitro.rive.EventPropertiesOutput import com.margelo.nitro.rive.EventPropertiesOutputExtensions as EPO import com.margelo.nitro.rive.RiveEventType +import com.margelo.nitro.rive.dropExtraNativeRef import com.margelo.nitro.rive.UnifiedRiveEvent as RNEvent import kotlinx.coroutines.CompletableDeferred @@ -398,7 +399,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { } catch (e: ViewModelException) { null }?.let { - val instance = it.createDefaultInstance() + val instance = it.createDefaultInstance().dropExtraNativeRef() stateMachine.viewModelInstance = instance } } @@ -411,7 +412,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { val file = riveAnimationView?.controller?.file if (artboard != null && file != null) { val viewModel = file.defaultViewModelForArtboard(artboard) - val instance = viewModel.createInstanceFromName(bindData.name) + val instance = viewModel.createInstanceFromName(bindData.name).dropExtraNativeRef() stateMachine.viewModelInstance = instance } }