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

未关闭
#3,955 1 条评论 1 个 reaction 已指派 0 人 在 GitHub 查看
主要语言
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

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。