Skip to content

fix(ios): prevent crash when FlutterEngine is destroyed during background#625

Open
IsmailAshour wants to merge 1 commit intodlutton:masterfrom
iStoriaE:fix/ios-engine-crash
Open

fix(ios): prevent crash when FlutterEngine is destroyed during background#625
IsmailAshour wants to merge 1 commit intodlutton:masterfrom
iStoriaE:fix/ios-engine-crash

Conversation

@IsmailAshour
Copy link

Summary

Fixes a FATAL crash on iOS when AVSpeechSynthesizerDelegate callbacks fire after the FlutterEngine has been destroyed.

Exception: NSInternalInconsistencyException: Sending a message before the FlutterEngine has been run.
Blame frame: @objc SwiftFlutterTtsPlugin.__ivar_destroyer

Root Cause

When the app enters the background, iOS may tear down TTS resources asynchronously. The AVSpeechSynthesizerDelegate methods (didFinish, didStart, didCancel, etc.) unconditionally call self.channel.invokeMethod(...) — but the FlutterEngine may no longer be running. This throws a fatal NSInternalInconsistencyException.

Three distinct crash paths observed in production:

Trigger iOS Stack Path
TTS thread termination TTSSpeechServerInstance terminateSpeechThreadchannel.invokeMethod
Audio session notification AVAudioSession privatePostNotificationForTypechannel.invokeMethod
Object deallocation TTSSpeechServerInstance deallocAXSpeechManager .cxx_destructchannel.invokeMethod

All crashes occur with processState: BACKGROUND.

Fix

  • Add isEngineAttached flag to track engine lifecycle state
  • Add safeInvokeMethod helper that guards channel.invokeMethod calls behind the flag
  • Add deinit to stop the synthesizer, nil the delegate, clear pending result callbacks, and set isEngineAttached = false — preventing delegate callbacks during deallocation
  • Guard didFinish delegate body with early return when engine is detached (protects speakResult/synthResult closure calls too)
  • Replace all 6 unguarded channel.invokeMethod calls with safeInvokeMethod

Affected Devices (from production Crashlytics)

  • iPhone X (iOS 16.7.12), iPhone 8 Plus (iOS 16.7.12), iPhone XS (iOS 15.4.1)
  • Versions 3.2.5 through 5.2.1 of our app
  • Regressed since v4.2.20

Related Issues

Fixes #558
Fixes #595
Related: #318, flutter/flutter#104637

…ound

Add lifecycle guard to prevent NSInternalInconsistencyException
"Sending a message before the FlutterEngine has been run" crash.

The crash occurs when iOS tears down TTS resources (AVSpeechSynthesizer
delegate callbacks) after the app enters background and the FlutterEngine
is no longer running. The delegate methods unconditionally call
channel.invokeMethod() which throws a fatal exception on a dead engine.

Changes:
- Add `isEngineAttached` flag to track engine lifecycle state
- Add `safeInvokeMethod` helper that guards channel calls
- Add `deinit` to stop synthesizer, nil delegate, and clear result
  callbacks before deallocation
- Guard `didFinish` delegate with early return when engine is detached
- Replace all 6 unguarded `channel.invokeMethod` calls with
  `safeInvokeMethod`

Fixes dlutton#558
Fixes dlutton#595
Related: dlutton#318

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IOS crash IOS crashes while running in the background:@objc flutter_tts.SwiftFlutterTtsPlugin.__ivar_destroyer

1 participant