Skip to content

Commit dbfe54c

Browse files
committed
fix: eagerly dispose RiveView and break referenced asset retain cycle
RiveView now calls dispose() on the native HybridObject when the React component unmounts, freeing the RiveView, ViewModel, and C++ scene graph immediately instead of waiting for Hermes GC. Also removes the withExtendedLifetime(fileRef) retain cycle in ReferencedAssetLoader that kept RiveFile alive permanently through the LoadAsset closure stored on the file itself.
1 parent 4fb46a1 commit dbfe54c

6 files changed

Lines changed: 71 additions & 44 deletions

File tree

android/src/main/java/com/margelo/nitro/rive/HybridRiveView.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
5151
private const val TAG = "HybridRiveView"
5252
}
5353

54+
//region Lifecycle
55+
override fun dispose() {
56+
view.dispose()
57+
}
58+
//endregion
59+
5460
//region State
5561
override val view: RiveReactNativeView = RiveReactNativeView(context)
5662
private var needsReload = false

ios/HybridRiveFileFactory.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,9 @@ final class HybridRiveFileFactory: HybridRiveFileFactorySpec, @unchecked Sendabl
3636

3737
let referencedAssetCache = SendableRef(ReferencedAssetCache())
3838
let factoryCache: SendableRef<RiveFactory?> = .init(nil)
39-
let fileRef: SendableRef<RiveFile?> = .init(nil)
4039
let customLoader = self.assetLoader.createCustomLoader(
4140
referencedAssets: referencedAssets, cache: referencedAssetCache,
42-
factory: factoryCache,
43-
fileRef: fileRef
41+
factory: factoryCache
4442
)
4543

4644
let riveFile =
@@ -49,7 +47,7 @@ final class HybridRiveFileFactory: HybridRiveFileFactorySpec, @unchecked Sendabl
4947
} else {
5048
try file(prepared)
5149
}
52-
fileRef.value = riveFile
50+
self.assetLoader.setFileRef(riveFile)
5351

5452
let result = (
5553
file: riveFile, cache: referencedAssetCache.value, factory: factoryCache.value,

ios/HybridRiveView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,13 @@ class HybridRiveView: HybridRiveViewSpec {
197197
}
198198
}
199199

200+
func dispose() {
201+
let riveView = view as? RiveReactNativeView
202+
DispatchQueue.main.async {
203+
riveView?.cleanup()
204+
}
205+
}
206+
200207
// MARK: Internal State
201208
private var needsReload = false
202209
private var dataBindingChanged = false

ios/ReferencedAssetLoader.swift

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,27 @@ func createAssetFileError(_ assetName: String) -> NitroRiveError {
1111
}
1212

1313
final class ReferencedAssetLoader {
14+
// Keeps the RiveFile alive while asset loads are in flight, without creating a retain cycle
15+
// through the LoadAsset closure stored on the file itself.
16+
private var activeLoadCount = 0
17+
private var activeFileRef: RiveFile?
18+
19+
func setFileRef(_ file: RiveFile) {
20+
activeFileRef = file
21+
}
22+
23+
private func retainFile() {
24+
activeLoadCount += 1
25+
}
26+
27+
private func releaseFile() {
28+
activeLoadCount -= 1
29+
if activeLoadCount <= 0 {
30+
activeLoadCount = 0
31+
activeFileRef = nil
32+
}
33+
}
34+
1435
private func handleRiveError(error: Error) {
1536
RCTLogError("\(error)")
1637
}
@@ -115,16 +136,15 @@ final class ReferencedAssetLoader {
115136

116137
func createCustomLoader(
117138
referencedAssets: ReferencedAssetsType?, cache: SendableRef<ReferencedAssetCache>,
118-
factory factoryOut: SendableRef<RiveFactory?>,
119-
fileRef: SendableRef<RiveFile?>
139+
factory factoryOut: SendableRef<RiveFactory?>
120140
)
121141
-> LoadAsset?
122142
{
123143
guard let referencedAssets = referencedAssets, let referencedAssets = referencedAssets.data
124144
else {
125145
return nil
126146
}
127-
return { (asset: RiveFileAsset, _: Data, factory: RiveFactory) -> Bool in
147+
return { [weak self] (asset: RiveFileAsset, _: Data, factory: RiveFactory) -> Bool in
128148
let assetByUniqueName = referencedAssets[asset.uniqueName()]
129149
guard let assetData = assetByUniqueName ?? referencedAssets[asset.name()] else {
130150
return false
@@ -133,10 +153,11 @@ final class ReferencedAssetLoader {
133153
cache.value[asset.uniqueName()] = asset
134154
factoryOut.value = factory
135155

136-
self.loadAssetInternal(
156+
self?.retainFile()
157+
self?.loadAssetInternal(
137158
source: assetData, asset: asset, factory: factory,
138-
completion: {
139-
withExtendedLifetime(fileRef) {}
159+
completion: { [weak self] in
160+
self?.releaseFile()
140161
})
141162

142163
return true

ios/RiveReactNativeView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ class RiveReactNativeView: UIView, RiveStateMachineDelegate {
242242
}
243243
}
244244

245-
private func cleanup() {
245+
func cleanup() {
246246
riveView?.removeFromSuperview()
247247
riveView?.stateMachineDelegate = nil
248248
riveView = nil

src/core/RiveView.tsx

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import type { ComponentProps } from 'react';
1+
import { useEffect, useRef, type ComponentProps } from 'react';
22
import { NitroRiveView } from './NitroRiveViewComponent';
33
import { RiveErrorType, type RiveError } from './Errors';
4+
import { callDispose } from './callDispose';
5+
import type { RiveViewRef } from '../index';
46

57
export interface RiveViewProps
68
extends Omit<ComponentProps<typeof NitroRiveView>, 'onError'> {
@@ -10,39 +12,32 @@ export interface RiveViewProps
1012
const defaultOnError = (error: RiveError) =>
1113
console.error(`[${RiveErrorType[error.type]}] ${error.message}`);
1214

13-
/**
14-
* RiveView is a React Native component that renders Rive graphics.
15-
* It provides a seamless way to display and control Rive graphics in your app.
16-
*
17-
* @example
18-
* ```tsx
19-
* <RiveView
20-
* file={riveFile}
21-
* artboardName="New Artboard"
22-
* stateMachineName="State Machine 1"
23-
* autoPlay={true}
24-
* fit={Fit.Contain}
25-
* style={styles.riveContainer}
26-
* />
27-
* ```
28-
*
29-
* @property {RiveFile} file - The Rive file to be displayed
30-
* @property {string} [artboardName] - Name of the artboard to display from the Rive file
31-
* @property {string} [stateMachineName] - Name of the state machine to play
32-
* @property {ViewModelInstance | DataBindMode | DataBindByName} [dataBind] - Data binding configuration for the state machine, defaults to DataBindMode.Auto
33-
* @property {boolean} [autoPlay=true] - Whether to automatically start playing the state machine
34-
* @property {Alignment} [alignment] - How the Rive graphic should be aligned within its container
35-
* @property {Fit} [fit] - How the Rive graphic should fit within its container
36-
* @property {Object} [style] - React Native style object for container customization
37-
* @property {(error: RiveError) => void} [onError] - Callback function that is called when an error occurs
38-
*
39-
* The component also exposes methods for controlling playback:
40-
* - play(): Starts playing the Rive graphic
41-
* - pause(): Pauses the Rive graphic
42-
*/
4315
export function RiveView(props: RiveViewProps) {
44-
const { onError, ...rest } = props;
16+
const { onError, hybridRef: userHybridRef, ...rest } = props;
4517
const wrappedOnError = onError ?? defaultOnError;
18+
const viewRef = useRef<RiveViewRef | null>(null);
4619

47-
return <NitroRiveView {...rest} onError={{ f: wrappedOnError }} />;
20+
useEffect(() => {
21+
return () => {
22+
if (viewRef.current) {
23+
callDispose(viewRef.current);
24+
viewRef.current = null;
25+
}
26+
};
27+
}, []);
28+
29+
const setRef = (ref: RiveViewRef) => {
30+
viewRef.current = ref;
31+
if (userHybridRef?.f) {
32+
userHybridRef.f(ref);
33+
}
34+
};
35+
36+
return (
37+
<NitroRiveView
38+
{...rest}
39+
onError={{ f: wrappedOnError }}
40+
hybridRef={{ f: setRef }}
41+
/>
42+
);
4843
}

0 commit comments

Comments
 (0)