Skip to content

fix(sensors_plus): isolate iOS stream handlers per engine - #3963

Open
FeliksLv01 wants to merge 1 commit into
fluttercommunity:mainfrom
FeliksLv01:fix/sensors-plus-ios-multi-engine
Open

FeliksLv01 wants to merge 1 commit into
fluttercommunity:mainfrom
FeliksLv01:fix/sensors-plus-ios-multi-engine

Conversation

@FeliksLv01

@FeliksLv01 FeliksLv01 commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes the iOS lifecycle crash and cross-engine stream interference described in #3955.

The plugin now publishes an engine-owned instance and implements detachFromEngine(for:). Event channels and stream handlers are instance-owned, while each handler owns its CoreMotion object and event sink. Detaching one FlutterEngine therefore only stops that engine's streams; queued callbacks re-check the handler state before sending an event.

The shared Swift handler base uses module visibility so the concrete handlers can be constructed from the plugin registration file.

Production crash evidence

An App Store crash on a physical iPhone running iOS 26.6.2 confirms the accelerometer callback path. The affected application uses a downstream sensors_plus fork, so its closure names differ from the upstream sendFlutter(...) helper. Inspection of that fork confirms that it also has the incorrect detachFromEngineForRegistrar(...) hook.

The application frames below were symbolicated against the exact distributed IPA. Both the application executable UUID and Flutter framework UUID match the crash report. Flutter's Objective-C metadata additionally identifies the engine and messenger-relay methods. Application identity, release/build numbers, binary UUIDs, installation paths, incident identifiers, and internal service links have been removed.

Exception Type: EXC_CRASH (SIGABRT)
Termination Reason: SIGNAL 6 Abort trap: 6
Triggered by Thread: 0

Last Exception Backtrace:
 0  CoreFoundation       __exceptionPreprocess + 164
 1  libobjc.A.dylib      objc_exception_throw + 88
 2  Foundation          -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 288
 3  Flutter             -[FlutterEngine sendOnChannel:message:binaryReply:] + 348
 4  Flutter             -[FlutterBinaryMessengerRelay sendOnChannel:message:] + 136
 5  Flutter             <unresolved frame; image offset 0x4d8da0>
 6  Application         thunk for @escaping @callee_unowned @convention(block) (@unowned Swift.AnyObject?) -> () + 192
 7  Application         closure #2 in closure #1 in FPPAccelerometerStreamHandlerPlus.onListen(withArguments:eventSink:) + 264
 8  Application         partial apply for closure #2 in closure #1 in FPPAccelerometerStreamHandlerPlus.onListen(withArguments:eventSink:) + 28
 9  Application         <deduplicated_symbol> + 28
10  libdispatch.dylib   _dispatch_call_block_and_release + 32
11  libdispatch.dylib   _dispatch_client_callout + 16
12  libdispatch.dylib   _dispatch_continuation_invoke_inline + 208
13  libdispatch.dylib   _dispatch_continuation_pop_inline + 476
14  libdispatch.dylib   _dispatch_main_queue_drain.cold.6 + 832
15  libdispatch.dylib   _os_tsd_set_direct + 16
16  libdispatch.dylib   _pthread_setspecific_direct + 16
17  libdispatch.dylib   _dispatch_thread_setspecific + 16
18  libdispatch.dylib   _dispatch_adopt_wlh_anon_recurse + 72
19  libdispatch.dylib   _dispatch_adopt_wlh_anon + 72
20  libdispatch.dylib   _dispatch_main_queue_drain + 176
21  libdispatch.dylib   _dispatch_main_queue_callback_4CF + 44
22  CoreFoundation     __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
23  CoreFoundation     __CFRunLoopRun + 1944
24  CoreFoundation     _CFRunLoopRunSpecificWithOptions + 532
25  GraphicsServices   GSEventRunModal + 120
26  UIKitCore          -[UIApplication _run] + 796
27  UIKitCore          UIApplicationMain + 332
28  UIKitCore          UIApplicationMain(_:_:_:_:) + 104
29  Application        main + 120
30  dyld               start + 6928

This preserves the complete last-exception backtrace, rather than including unrelated background threads. A matching dSYM is not available, so source line numbers and the stripped Flutter frame are deliberately left unresolved. The corresponding production Sentry exception is NSInternalInconsistencyException: Sending a message before the FlutterEngine has been run.; the Apple report independently shows the assertion in the engine's send method. This is evidence of a callback reaching an unavailable engine, not evidence of an out-of-memory termination.

Related Issues

Checklist

  • I read the Contributor Guide and followed the required package-specific branch and PR workflow.
  • I titled the PR using Conventional Commits.
  • I did not modify the CHANGELOG.md nor the plugin version in pubspec.yaml files.
  • All existing and new tests are passing.
  • The analyzer does not report any problems on this PR.

Validation

  • Built the iOS simulator example with Flutter 3.47.8, using a local path override for this PR's sensors_plus sources:
    flutter build ios --no-codesign --simulator --debug --target=lib/main.dart --dart-define=CI=true.
  • Swift type-check of both modified source files against the iOS simulator SDK.
  • Matched both executable UUIDs and symbolicated the production crash against the distributed IPA.
  • git diff --check upstream/main...HEAD.

Breaking Change

  • Yes, this is a breaking change.
  • No, this is not a breaking change.

@FeliksLv01
FeliksLv01 force-pushed the fix/sensors-plus-ios-multi-engine branch from 2f5d49f to 88c5402 Compare November 12, 2026 06:18
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.

[Bug]: [sensors_plus] Termination crash fix from 1.3.3 is unreachable: plugin instance never published, detach hook has wrong selector

1 participant