@@ -36,41 +36,59 @@ final class ReferencedAssetLoader {
3636 RCTLogError ( " \( error) " )
3737 }
3838
39+ /// Decodes an asset and applies the result on the main thread.
40+ ///
41+ /// - `onMain: true` — decode + apply run synchronously on the caller's
42+ /// thread (must already be main). Used for `decodeImage` which is not
43+ /// thread-safe.
44+ /// - `onMain: false` — decode runs on a serial background queue, then
45+ /// apply + completion dispatch to main. The `[self]` capture keeps the
46+ /// `ReferencedAssetLoader` (and its `activeFileRef`) alive until the
47+ /// main-thread block completes, preventing use-after-free on the factory.
48+ private func decodeAndApply< T> (
49+ onMain: Bool ,
50+ decode: @escaping ( ) -> T ? ,
51+ apply: @escaping ( T ) -> Void ,
52+ completion: @escaping ( ) -> Void
53+ ) {
54+ if onMain {
55+ if let result = decode ( ) { apply ( result) }
56+ completion ( )
57+ } else {
58+ Self . decodeQueue. async { [ self ] in
59+ let result = decode ( )
60+ DispatchQueue . main. async {
61+ if let result { apply ( result) }
62+ completion ( )
63+ _ = self
64+ }
65+ }
66+ }
67+ }
68+
3969 private func processAssetBytes(
4070 _ data: Data , asset: RiveFileAsset , factory: RiveFactory , completion: @escaping ( ) -> Void
4171 ) {
42- if data. isEmpty == true {
72+ if data. isEmpty {
4373 completion ( )
4474 return
4575 }
4676 switch asset {
4777 case let imageAsset as RiveImageAsset :
48- // decodeImage is not thread-safe — decode on main thread, synchronously
49- // within the current MainActor context (caller is already on main).
50- let decodedImage = factory. decodeImage ( data)
51- imageAsset. renderImage ( decodedImage)
52- completion ( )
78+ decodeAndApply ( onMain: true ,
79+ decode: { factory. decodeImage ( data) } ,
80+ apply: { imageAsset. renderImage ( $0) } ,
81+ completion: completion)
5382 case let fontAsset as RiveFontAsset :
54- Self . decodeQueue. async { [ self ] in
55- let decodedFont = factory. decodeFont ( data)
56- DispatchQueue . main. async {
57- fontAsset. font ( decodedFont)
58- completion ( )
59- _ = self
60- }
61- }
83+ decodeAndApply ( onMain: false ,
84+ decode: { factory. decodeFont ( data) } ,
85+ apply: { fontAsset. font ( $0) } ,
86+ completion: completion)
6287 case let audioAsset as RiveAudioAsset :
63- Self . decodeQueue. async { [ self ] in
64- guard let decodedAudio = factory. decodeAudio ( data) else {
65- DispatchQueue . main. async { completion ( ) }
66- return
67- }
68- DispatchQueue . main. async {
69- audioAsset. audio ( decodedAudio)
70- completion ( )
71- _ = self
72- }
73- }
88+ decodeAndApply ( onMain: false ,
89+ decode: { factory. decodeAudio ( data) } ,
90+ apply: { audioAsset. audio ( $0) } ,
91+ completion: completion)
7492 default :
7593 completion ( )
7694 }
0 commit comments