diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 6e16c751..782dd595 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1357,7 +1357,7 @@ PODS: - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - - react-native-safe-area-context (5.6.2): + - react-native-safe-area-context (5.7.0): - DoubleConversion - glog - hermes-engine @@ -1372,8 +1372,8 @@ PODS: - React-hermes - React-ImageManager - React-jsi - - react-native-safe-area-context/common (= 5.6.2) - - react-native-safe-area-context/fabric (= 5.6.2) + - react-native-safe-area-context/common (= 5.7.0) + - react-native-safe-area-context/fabric (= 5.7.0) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -1383,7 +1383,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-safe-area-context/common (5.6.2): + - react-native-safe-area-context/common (5.7.0): - DoubleConversion - glog - hermes-engine @@ -1407,7 +1407,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-safe-area-context/fabric (5.6.2): + - react-native-safe-area-context/fabric (5.7.0): - DoubleConversion - glog - hermes-engine @@ -1754,7 +1754,7 @@ PODS: - React-logger (= 0.79.2) - React-perflogger (= 0.79.2) - React-utils (= 0.79.2) - - RiveRuntime (6.18.2) + - RiveRuntime (6.20.4) - RNCAsyncStorage (2.2.0): - DoubleConversion - glog @@ -1904,7 +1904,7 @@ PODS: - ReactCommon/turbomodule/core - RNWorklets - Yoga - - RNRive (0.4.2): + - RNRive (0.4.7): - DoubleConversion - glog - hermes-engine @@ -1928,7 +1928,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RiveRuntime (= 6.18.2) + - RiveRuntime (= 6.20.4) - Yoga - RNScreens (4.18.0): - DoubleConversion @@ -2347,7 +2347,7 @@ SPEC CHECKSUMS: React-logger: 8edfcedc100544791cd82692ca5a574240a16219 React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468 React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6 - react-native-safe-area-context: 0b8555c40461feb7198e999912a3446602e7c601 + react-native-safe-area-context: 26634d9b636a98ceee20cb6fa5dc946922f1e90f React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d @@ -2379,12 +2379,12 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 04d5eb15eb46be6720e17a4a7fa92940a776e584 ReactCodegen: c63eda03ba1d94353fb97b031fc84f75a0d125ba ReactCommon: 76d2dc87136d0a667678668b86f0fca0c16fdeb0 - RiveRuntime: 55c7a7badd9a8389d20fc8a75b7c6accc851b69a + RiveRuntime: f99ddd1d4b6a420dea5943ff52502e8f92ae7a92 RNCAsyncStorage: a1c8cc8a99c32de1244a9cf707bf9d83d0de0f71 RNCPicker: 28c076ae12a1056269ec0305fe35fac3086c477d RNGestureHandler: 6b39f4e43e4b3a0fb86de9531d090ff205a011d5 RNReanimated: 66b68ebe3baf7ec9e716bd059d700726f250d344 - RNRive: c02b3545abcf477d074945c5103f9f4bc9d8d672 + RNRive: 042cbd7de1e5dd7e11aac076ade185926717543b RNScreens: f38464ec1e83bda5820c3b05ccf4908e3841c5cc RNWorklets: b1faafefb82d9f29c4018404a0fb33974b494a7b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 diff --git a/ios/ReferencedAssetLoader.swift b/ios/ReferencedAssetLoader.swift index 892b8ef0..43825772 100644 --- a/ios/ReferencedAssetLoader.swift +++ b/ios/ReferencedAssetLoader.swift @@ -11,6 +11,7 @@ func createAssetFileError(_ assetName: String) -> NitroRiveError { } final class ReferencedAssetLoader { + private static let decodeQueue = DispatchQueue(label: "com.rive.asset-decode") private var activeLoadCount = 0 private var activeFileRef: RiveFile? @@ -35,46 +36,64 @@ final class ReferencedAssetLoader { RCTLogError("\(error)") } - private func processAssetBytes( - _ data: Data, asset: RiveFileAsset, factory: RiveFactory, completion: @escaping () -> Void + /// Decodes an asset and applies the result on the main thread. + /// + /// - `onMain: true` — decode + apply run synchronously on the caller's + /// thread (must already be main). Used for `decodeImage` which is not + /// thread-safe. + /// - `onMain: false` — decode runs on a serial background queue, then + /// apply + completion dispatch to main. The `[self]` capture keeps the + /// `ReferencedAssetLoader` (and its `activeFileRef`) alive until the + /// main-thread block completes, preventing use-after-free on the factory. + private func decodeAndApply( + onMain: Bool, + decode: @escaping () -> T?, + apply: @escaping (T) -> Void, + completion: @escaping () -> Void ) { - if data.isEmpty == true { + if onMain { + if let result = decode() { apply(result) } completion() - return - } - DispatchQueue.global(qos: .background).async { - switch asset { - case let imageAsset as RiveImageAsset: - let decodedImage = factory.decodeImage(data) - DispatchQueue.main.async { - imageAsset.renderImage(decodedImage) - completion() - } - case let fontAsset as RiveFontAsset: - let decodedFont = factory.decodeFont(data) - DispatchQueue.main.async { - fontAsset.font(decodedFont) - completion() - } - case let audioAsset as RiveAudioAsset: - guard let decodedAudio = factory.decodeAudio(data) else { - DispatchQueue.main.async { - completion() - } - return - } - DispatchQueue.main.async { - audioAsset.audio(decodedAudio) - completion() - } - default: + } else { + Self.decodeQueue.async { [self] in + let result = decode() DispatchQueue.main.async { + if let result { apply(result) } completion() + _ = self } } } } + private func processAssetBytes( + _ data: Data, asset: RiveFileAsset, factory: RiveFactory, completion: @escaping () -> Void + ) { + if data.isEmpty { + completion() + return + } + switch asset { + case let imageAsset as RiveImageAsset: + decodeAndApply(onMain: true, + decode: { factory.decodeImage(data) }, + apply: { imageAsset.renderImage($0) }, + completion: completion) + case let fontAsset as RiveFontAsset: + decodeAndApply(onMain: false, + decode: { factory.decodeFont(data) }, + apply: { fontAsset.font($0) }, + completion: completion) + case let audioAsset as RiveAudioAsset: + decodeAndApply(onMain: false, + decode: { factory.decodeAudio(data) }, + apply: { audioAsset.audio($0) }, + completion: completion) + default: + completion() + } + } + private func handlePreloadedImage( _ image: any HybridRiveImageSpec, asset: RiveFileAsset, completion: @escaping () -> Void ) { diff --git a/package.json b/package.json index be366390..62f946aa 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ }, "homepage": "https://github.com/rive-app/rive-nitro-react-native#readme", "runtimeVersions": { - "ios": "6.18.2", + "ios": "6.20.4", "android": "11.4.0" }, "publishConfig": {