fluttercommunity / fluttercommunity/plus_plugins
[Bug]: [sensors_plus] Termination crash fix from 1.3.3 is unreachable: plugin instance never published, detach hook has wrong selector
- 主要语言
- Dart
- 星标
- 1.9k
- 派生
- 1.3k
- PR 合并指标
- 30 天内没有已合并 PR
描述
### Platform
iOS (crash observed on Mac Catalyst / "iPad app on Mac", iOS 18.6 and 26.5 hosts; the affected code path is the shared iOS plugin)
### Plugin
sensors_plus
### Version
7.1.0 (latest at time of filing; 7.0.0 identical in the affected code)
### Flutter SDK
3.41.6
### Steps to reproduce
1. Subscribe to `accelerometerEventStream()` (any motion stream reproduces it).
2. Run the app on Mac Catalyst ("Designed for iPad" on Apple Silicon).
3. Quit the app (Cmd+Q / `-[NSApplication terminate:]`) while the subscription is active, or shortly after cancelling it.
4. The process dies with an uncaught `NSInternalInconsistencyException`: `Sending a message before the FlutterEngine has been run.`
This is the same crash previously reported in #956, #862, #863 (and the connectivity_plus twin #865). Those were closed after the `_isCleanUp` guards were added in sensors_plus 1.3.3, but **the fix is unreachable code in every release since** - details below. We still receive these crashes from production users (Sentry, symbolicated: `FPPStreamHandlerPlus.swift` `sendFlutter` -> `sink(FlutterStandardTypedData...)` -> `-[FlutterEngine sendOnChannel:message:binaryReply:]` `NSAssert`, drained from the main queue inside `-[NSApplication _shouldTerminate]`, with `com.apple.CoreMotion.MotionThread` still live at termination).
### Why the existing `_isCleanUp` fix never runs
Three independent reasons, all in `ios/sensors_plus/Sources/sensors_plus/`:
1. **No plugin instance is ever published.** `FPPSensorsPlusPlugin.register(with:)` is entirely static: it never constructs the plugin and never calls `registrar.publish(...)`, so the registrar holds no object to deliver a detach callback to. (Compare `video_player_avfoundation`'s `register(with:)`, which publishes the instance specifically "so that it receives detachFromEngine".)
2. **The detach hook has the wrong signature.** It is declared `func detachFromEngineForRegistrar(registrar: NSObject!)`. The `FlutterPlugin` protocol member imports into Swift as `detachFromEngine(for registrar: FlutterPluginRegistrar)`; the declared method is neither `public` nor that selector, so it satisfies no protocol requirement and is never invoked. `_cleanUp()` therefore never runs and `_isCleanUp` stays `false` forever. Every other Swift plugin in our dependency set (`connectivity_plus`, `record_ios`, `video_player_avfoundation`, `camera_avfoundation`) uses the correct signature; sensors_plus is the sole outlier.
3. **The `dealloc()` methods on the stream handlers are dead too.** ARC never calls a Swift method named `dealloc`.
And even with the flag flipped, every `if _isCleanUp { return }` check runs **before** the `DispatchQueue.main.async` post in `sendFlutter` (and in the barometer handler's own async block), so it cannot stop a block that is already sitting on the main queue when teardown begins. On Catalyst quit, AppKit drains exactly those blocks after the FlutterEngine is destroyed.
Note that app-side teardown cannot fix this: cancelling the Dart `StreamSubscription` on `AppLifecycleState.detached` is an async platform-channel round trip, and CoreMotion callbacks already in flight have already posted their main-queue blocks. We ship that teardown and still crash.
### Suggested fix (tested as a vendored patch)
Four minimal edits. We run these in production as a vendored copy; happy to open a PR.
```diff
--- a/ios/sensors_plus/Sources/sensors_plus/FPPSensorsPlusPlugin.swift
+++ b/ios/sensors_plus/Sources/sensors_plus/FPPSensorsPlusPlugin.swift
@@ public class FPPSensorsPlusPlugin: NSObject, FlutterPlugin {
_isCleanUp = false
+
+ // Publish the instance so that it receives detachFromEngine.
+ let instance = FPPSensorsPlusPlugin()
+ registrar.publish(instance)
}
- func detachFromEngineForRegistrar(registrar: NSObject!) {
+ public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
FPPSensorsPlusPlugin._cleanUp()
}
```
```diff
--- a/ios/sensors_plus/Sources/sensors_plus/FPPStreamHandlerPlus.swift
+++ b/ios/sensors_plus/Sources/sensors_plus/FPPStreamHandlerPlus.swift
@@ func sendFlutter(...)
DispatchQueue.main.async {
+ // The pre-post check cannot stop a block that is already queued when
+ // teardown starts; re-check before touching the sink.
+ if _isCleanUp {
+ return
+ }
let timestampSince1970Micro = timestampMicroAtBoot + (timestamp * 1000000)
```
(same re-check inside the barometer handler's own `DispatchQueue.main.async` block)
```diff
func onCancel(withArguments arguments: Any?) -> FlutterError? {
- _motionManager.stopAccelerometerUpdates()
+ _motionManager?.stopAccelerometerUpdates()
```
The fourth edit (nil-safe `onCancel` on all five handlers) is **required**, not polish: `_motionManager` / `_altimeter` are implicitly-unwrapped globals created lazily in `onListen`, and `_cleanUp()` cancels every registered handler including ones that never listened. Today that nil-deref is masked only because `_cleanUp()` is dead code; once the detach hook actually runs, an app that never listened to (say) the barometer would crash on `_altimeter.stopRelativeAltitudeUpdates()` at every engine detach.
### Code Sample
```dart
// Any minimal app that keeps a subscription alive:
final sub = accelerometerEventStream(
samplingPeriod: SensorInterval.uiInterval,
).listen((e) {});
// Run on Mac Catalyst, then quit the app with Cmd+Q.
```
### Logs
```shell
Fatal Exception: NSInternalInconsistencyException
Sending a message before the FlutterEngine has been run.
Symbolicated (Sentry, production, sensors_plus 7.1.0 / Flutter 3.41.6):
-[NSApplication terminate:] / _handleAEQuit
-[NSApplication _shouldTerminate]
_dispatch_main_queue_drain
closure #1 in sendFlutter(x:y:z:timestamp:sink:) FPPStreamHandlerPlus.swift:45
Array.withUnsafeBufferPointer / sink(FlutterStandardTypedData...) FPPStreamHandlerPlus.swift:46
SetStreamHandlerMessageHandlerOnChannel FlutterChannels.mm:400
-[FlutterEngine sendOnChannel:message:binaryReply:] FlutterEngine.mm:1315 (NSAssert -> abort)
Thread com.apple.CoreMotion.MotionThread live at termination.
```
### Flutter Doctor
```shell
Flutter 3.41.6 (stable), Dart 3.11.4, DevTools 2.54.2
Crash observed in release builds shipped via the App Store; not toolchain-specific.
```
### Checklist before submitting a bug
- [x] I searched issues in this repository and couldn't find such bug/problem
- [x] I Google'd a solution and I couldn't find it
- [x] I searched on StackOverflow for a solution and I couldn't find it
- [x] I read the README.md file of the plugin
- [x] I'm using the latest version of the plugin
- [x] All dependencies are up to date with `flutter pub upgrade`
- [x] I did a `flutter clean`
- [x] I tried running the example project
贡献指南
调研方向
从 ios/sensors_plus/Sources/sensors_plus/FPPSensorsPlusPlugin.swift 和 stream-handler 文件开始,将 detach hook 和实例发布方式与 issue 中提到的其他 Swift 插件进行比较。在 Mac Catalyst 上使用处于活动状态的加速度计订阅,复现示例应用的崩溃。完成的标准是:engine teardown 不再发送排队中的传感器事件,也不再崩溃,包括 handler 从未监听过的情况。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- dart, swift
- 领域
- mobile-dev
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 活跃
- 描述清晰度
- 描述清楚
- 新手友好度
- 72/100