Skip to content

Crash in app.rive.command-server during CommandServer teardown after RiveUIView deallocation #442

@wlixcc

Description

@wlixcc

Submission checklist

  • I have confirmed the issue is present in the latest version of RiveRuntime
  • I have searched the documentation and forums and could not find an answer
  • I have searched existing issues and this is not a duplicate

Description

Description

We are seeing an intermittent production crash from Crashlytics when a RiveUIView used as a launch screen animation is removed and released.

The crash happens on Rive's background command server thread during teardown:

Crashed: app.rive.command-server
0  libdispatch.dylib   _dispatch_semaphore_dispose.cold.1
1  libdispatch.dylib   _dispatch_semaphore_signal_slow
2  libdispatch.dylib   _dispatch_dispose
3  RiveRuntime         RiveHitResultFromRuntime(rive::HitResult)
...
11 RiveRuntime         rive::CommandServer::~CommandServer()
12 RiveRuntime         rive::CommandServer::~CommandServer()

This may be related to other lifecycle / teardown issues in the new async runtime, especially:

However, this crash stack looks different because it happens during rive::CommandServer destruction, with _dispatch_semaphore_dispose / _dispatch_semaphore_signal_slow at the top of the crashed thread.

Previous working version

No response

Rive Apple runtime version

6.19.0

Reproduction steps / code

Reproduction steps / code

We have not been able to reproduce this deterministically in a small sample app yet. The production crash happens during launch-screen teardown, when a temporary RiveUIView is removed from the view hierarchy and the owning view is released.

The relevant usage pattern is:

  1. Create a temporary UIKit launch screen view.
  2. Inside that view, create a RiveUIView(rive: nil).
  3. Load a local .riv file using the new async runtime:
    • Worker
    • File
    • Artboard
    • StateMachine
    • ViewModelInstance
    • Rive
  4. Assign the resulting Rive instance to riveView.rive.
  5. After the launch flow completes, remove the launch screen from the view hierarchy.
  6. Immediately release the owning launch screen view.
  7. Crashlytics intermittently reports a crash on app.rive.command-server during rive::CommandServer teardown.

Reduced version of the production code:

import UIKit
import RiveRuntime

final class LaunchScreenView: UIView {
    private let phaseProperty = NumberProperty(path: "phase")
    private var launchViewModelInstance: ViewModelInstance?
    private var riveLoadTask: Task<Void, Never>?

    private lazy var riveView: RiveUIView = {
        let view = RiveUIView(rive: nil)
        view.backgroundColor = .clear
        return view
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        addSubview(riveView)
        riveView.frame = bounds
        riveView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        loadRiveAnimation()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit {
        riveLoadTask?.cancel()
    }

    private func loadRiveAnimation() {
        riveLoadTask = Task { [weak self] in
            await self?.prepareRiveAnimation()
        }
    }

    @MainActor
    private func prepareRiveAnimation() async {
        do {
            let worker = try await Worker()
            let file = try await File(source: .local("launch", Bundle.main), worker: worker)
            let artboard = try await file.createArtboard()
            let stateMachine = try await artboard.createStateMachine("LaunchStateMachine")

            let viewModelInstance = try await file.createViewModelInstance(
                .blank(from: .name("LaunchViewModel"))
            )
            viewModelInstance.setValue(of: phaseProperty, to: 1)

            let rive = try await Rive(
                file: file,
                artboard: artboard,
                stateMachine: stateMachine,
                dataBind: .instance(viewModelInstance),
                fit: .contain(alignment: .center)
            )

            launchViewModelInstance = viewModelInstance
            riveView.rive = rive
        } catch {
            print("Failed to load Rive launch animation: \(error)")
        }
    }

    @MainActor
    func updatePhase(_ phase: Float) {
        launchViewModelInstance?.setValue(of: phaseProperty, to: phase)
    }
}

The launch view is owned by a UITabBarController and released immediately after the launch transition finishes:

final class RootTabBarController: UITabBarController {
    private var launchScreenView: LaunchScreenView? = LaunchScreenView()

    func startLaunchTransition() {
        guard let launchScreenView else { return }

        view.addSubview(launchScreenView)
        launchScreenView.frame = view.bounds
        launchScreenView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        launchScreenView.updatePhase(1)

        DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) { [weak self] in
            self?.finishLaunchTransition()
        }
    }

    private func finishLaunchTransition() {
        guard let launchScreenView else { return }

        UIView.animate(withDuration: 0.25) {
            launchScreenView.alpha = 0
        } completion: { [weak self] _ in
            launchScreenView.removeFromSuperview()

            // Releasing this temporary view releases RiveUIView / Rive / Worker.
            // Crashlytics intermittently reports a crash shortly after this,
            // on the `app.rive.command-server` thread.
            self?.launchScreenView = nil
        }
    }
}

The Crashlytics stack suggests the crash happens after this release path, while RiveRuntime is tearing down the worker command server:

Crashed: app.rive.command-server
0  libdispatch.dylib   _dispatch_semaphore_dispose.cold.1
1  libdispatch.dylib   _dispatch_semaphore_signal_slow
2  libdispatch.dylib   _dispatch_dispose
3  RiveRuntime         RiveHitResultFromRuntime(rive::HitResult)
...
11 RiveRuntime         rive::CommandServer::~CommandServer()
12 RiveRuntime         rive::CommandServer::~CommandServer()


### Upload your reproduction files / stack trace

_No response_

### Source `.riv` / `.rev` file

_No response_

### Screenshots / video

_No response_

### Device

iPad (11th generation),iPhone 11,iPhone XS Max

### Apple OS version

iPadOS 26.2.0,iOS 16.1.1,iOS 17.1.2,iOS 15.8.4

### Additional context

_No response_

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions