Commit d92574d
authored
fix(iOS): use per-call ReferencedAssetLoader to prevent concurrent access crash (#218)
Fixes #215
Regression from #202 — the shared `ReferencedAssetLoader` instance on
`HybridRiveFileFactory` caused a data race when multiple `RiveFile`
loads ran concurrently (e.g. mounting several `RiveView` components
simultaneously).
`HybridRiveFileFactory.genericFrom` dispatches file parsing to
`DispatchQueue.global(qos: .userInitiated)`. With multiple views
mounting at the same time, multiple GCD worker threads called
`self.assetLoader.setFileRef(riveFile)` on the same shared instance
concurrently. Since Swift property assignment (`activeFileRef = file`)
is not atomic — it involves load-old, store-new, release-old as separate
steps — two threads could both load the same old value and both release
it, causing a double-release (use-after-free) of the previous
`RiveFile`.
The fix creates a new `ReferencedAssetLoader` per
`HybridRiveFileFactory.genericFrom` call instead of sharing one on the
factory. This matches the Android implementation
(`HybridRiveFileFactory.kt` already creates `val loader =
ReferencedAssetLoader()` per call). Verified that the retain cycle fix
from #202 is not regressed (memgraph shows no leaked `RiveFile` after
navigating away from referenced asset screens).
### Reproducer
Rapidly mount/unmount multiple `RiveView` components (8+ views toggling
at 10-50ms intervals, 1000+ cycles). Requires Expo SDK 55 / RN 0.83.4 —
does not reproduce on Expo SDK 54 / RN 0.81.
<details>
<summary>Reproducer component (Issue215.tsx)</summary>
```tsx
import { useEffect, useState, useCallback } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import {
Fit,
RiveView,
useRive,
useRiveFile,
useViewModelInstance,
} from '@rive-app/react-native';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
const RIVE_URL = 'https://cdn.rive.app/animations/vehicles.riv';
function RiveCell({ source, label }: { source: any; label: string }) {
const { riveFile } = useRiveFile(source);
const { instance } = useViewModelInstance(riveFile);
const { setHybridRef } = useRive();
return (
<Animated.View
entering={FadeIn.duration(150)}
exiting={FadeOut.duration(150)}
style={styles.riveContainer}
>
<Text style={styles.label}>{label}</Text>
{riveFile && (
<RiveView
file={riveFile}
autoPlay
fit={Fit.Contain}
style={{ flex: 1 }}
hybridRef={setHybridRef}
{...(instance ? { dataBind: instance } : {})}
/>
)}
</Animated.View>
);
}
function LoggedInScreens() {
return (
<View style={styles.loggedIn}>
<Text style={styles.title}>Logged In</Text>
<View style={styles.riveGrid}>
<RiveCell source={RIVE_URL} label="A" />
<RiveCell source={RIVE_URL} label="B" />
<RiveCell source={RIVE_URL} label="C" />
<RiveCell source={RIVE_URL} label="D" />
<RiveCell source={RIVE_URL} label="E" />
<RiveCell source={RIVE_URL} label="F" />
<RiveCell source={RIVE_URL} label="G" />
<RiveCell source={RIVE_URL} label="H" />
</View>
</View>
);
}
function LandingScreen() {
return (
<Animated.View
entering={FadeIn.duration(150)}
exiting={FadeOut.duration(150)}
style={styles.landing}
>
<Text style={styles.title}>Landing</Text>
<RiveCell source={RIVE_URL} label="Landing A" />
<RiveCell source={RIVE_URL} label="Landing B" />
<RiveCell source={RIVE_URL} label="Landing C" />
<RiveCell source={RIVE_URL} label="Landing D" />
</Animated.View>
);
}
export default function Issue215Page() {
const [isLoggedIn, setIsLoggedIn] = useState(true);
const [isRapid, setIsRapid] = useState(false);
const [count, setCount] = useState(0);
const toggle = useCallback(() => setIsLoggedIn((v) => !v), []);
useEffect(() => {
if (!isRapid) return;
let c = 0;
let timeout: ReturnType<typeof setTimeout>;
const scheduleNext = () => {
const delay = Math.floor(Math.random() * 41) + 10;
timeout = setTimeout(() => {
toggle();
c++;
setCount(c);
if (c > 5000) {
setIsRapid(false);
} else {
scheduleNext();
}
}, delay);
};
scheduleNext();
return () => clearTimeout(timeout);
}, [isRapid, toggle]);
return (
<SafeAreaView style={styles.container}>
<Pressable
style={styles.rapidButton}
onPress={() => { setIsRapid((r) => !r); setCount(0); }}
>
<Text style={styles.buttonText}>
{isRapid ? `Toggling... (${count}/5000)` : 'Start rapid toggle (10–50ms random)'}
</Text>
</Pressable>
<Pressable style={styles.manualButton} onPress={toggle}>
<Text style={styles.buttonText}>
{isLoggedIn ? 'Manual logout' : 'Manual login'}
</Text>
</Pressable>
{isLoggedIn ? <LoggedInScreens /> : <LandingScreen />}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0c1027' },
loggedIn: { flex: 1, padding: 8 },
landing: { flex: 1, padding: 8 },
title: { color: '#FFF', fontSize: 20, fontWeight: '600', textAlign: 'center', marginBottom: 8 },
label: { color: '#FFF', fontSize: 10, fontWeight: '600', textAlign: 'center', paddingVertical: 2 },
riveGrid: { flex: 1, flexDirection: 'row', flexWrap: 'wrap', gap: 6 },
riveContainer: { width: '48%', height: 120, backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 8, overflow: 'hidden' },
rapidButton: { backgroundColor: '#a03030', paddingVertical: 10, paddingHorizontal: 24, borderRadius: 12, alignSelf: 'center', marginTop: 8 },
manualButton: { backgroundColor: '#30a050', paddingVertical: 8, paddingHorizontal: 20, borderRadius: 12, alignSelf: 'center', marginTop: 8 },
buttonText: { color: '#FFF', fontSize: 13, fontWeight: '600', textAlign: 'center' },
});
```
</details>1 parent e9d648b commit d92574d
1 file changed
Lines changed: 6 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
6 | 5 | | |
7 | 6 | | |
8 | 7 | | |
| |||
34 | 33 | | |
35 | 34 | | |
36 | 35 | | |
| 36 | + | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
54 | | - | |
| 56 | + | |
55 | 57 | | |
56 | 58 | | |
57 | 59 | | |
| |||
0 commit comments