You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter应用后台或闲置状态下接收Pusher/Socket消息的问题咨询

问题解决方案

一、根因说明

移动端Android/iOS系统都存在后台资源限制机制,应用退后台超过3-5分钟后,系统会主动回收非活跃的长连接资源,无论用Pusher还是Socket.io都无法完全规避该系统限制,后台期间的消息需配合系统推送服务补触达,前台恢复后需要主动重连恢复长连接。
你现有代码还有两个逻辑缺陷放大了该问题:

  • 未监听应用前后台切换状态,前台恢复时没有主动检查连接状态
  • disconnect方法直接关闭了全局StreamController,重连后无法继续接收事件
  • connect方法仅在_pusher为null时初始化,已初始化过但断开的连接不会被重新激活

二、Pusher兼容双端修复方案

2.1 修改pusher.dart逻辑

class AppPusher {
  static final _appPusher = AppPusher._internal();
  AppPusher._internal();
  factory AppPusher() => _appPusher;

  final String _messaging = 'messaging';
  final String _notifications = 'notifications';

  PusherClient? _pusher;
  Channel? _channel;
  String? _channelName;
  // 不要在普通断开操作里关闭该控制器,全局复用
  final _pusherData = StreamController<SocketResult>.broadcast();
  Stream<SocketResult> get stream => _pusherData.stream;

  // 新增:获取当前连接状态
  bool get isConnected => _pusher?.connectionState == ConnectionState.CONNECTED;

  void connect({required Me me}) {
    final token = _getToken();
    // 已有实例且已连接,直接返回
    if (_pusher != null && isConnected) return;

    // 已有实例但断开,先清理旧资源
    if (_pusher != null) {
      _cleanOldResource();
    }

    _pusher = PusherClient(
      AppSettings.pusher.key,
      PusherOptions(
        cluster: AppSettings.pusher.cluster,
        encrypted: true,
        auth: PusherAuth(
          AppSettings.pusher.url,
          headers: {'Authorization': 'Bearer $token'},
        ),
        // 新增:开启活动检测,避免连接假死
        activityTimeout: 30000,
        pongTimeout: 10000,
      ),
      autoConnect: true,
      enableLogging: AppSettings.pusher.enableLogging,
    );

    // 新增:监听连接状态变化,断开自动重连
    _pusher!.onConnectionStateChange((state) {
      if (state?.currentState == ConnectionState.DISCONNECTED) {
        // 延迟2秒重连,避免频繁重试
        Future.delayed(const Duration(seconds: 2), () => connect(me: me));
      }
    });

    _channelName = 'private-user-${me.id}';
    _channel = _pusher!.subscribe(_channelName!);

    _channel!.bind(_messaging, (event) {
      if (event?.data != null) {
        _pusherData.sink.add(SocketResult<SocketMessage>(
          type: ChannelType.message,
          me: me,
          data: SocketMessage.fromJson(jsonDecode(event.data!)),
        ));
      }
    });

    _channel!.bind(_notifications, (event) {
      if (event?.data != null) {
        _pusherData.sink.add(SocketResult<SocketNotification>(
          type: ChannelType.notification,
          me: me,
          data: SocketNotification.fromJson(jsonDecode(event.data!)),
        ));
      }
    });
  }

  // 新增:清理旧连接资源,不关闭StreamController
  void _cleanOldResource() {
    if (_pusher == null) return;
    if (_channel != null) {
      _channel!.unbind(_messaging);
      _channel!.unbind(_notifications);
    }
    if (_channelName != null) {
      _pusher!.unsubscribe(_channelName!);
    }
    _pusher!.disconnect();
    _pusher = null;
  }

  // 仅在应用完全退出时调用该方法
  void dispose() async {
    _cleanOldResource();
    await _pusherData.close();
  }

  String _getToken() {
    return locator<SharedPreferencesManager>()
            .getString(SharedPreferencesManager.accessToken) ??
        '';
  }
}

2.2 修改app.dart逻辑,添加生命周期监听

// 混入WidgetsBindingObserver监听生命周期
class _AppState extends State<App> with WidgetsBindingObserver {
  late StreamSubscription _streamSubscription;
  final AppPusher _pusher = AppPusher();

  void initPusher() {
    final me = context.read<MeProvider>().me!;
    _pusher.connect(me: me);
    // 避免重复监听,先取消旧订阅
    _streamSubscription.cancel();
    _streamSubscription = _pusher.stream.listen((event) {
      switch (event.type) {
        case ChannelType.message:
          context
              .read<ChatProvider>()
              .socketMessage(message: event.data, me: event.me);
          break;
        case ChannelType.notification:
          print('EVENT FROM NOTIFICATION: ${event.data}');
          break;
      }
    });
  }

  @override
  void initState() {
    super.initState();
    // 注册生命周期监听
    WidgetsBinding.instance.addObserver(this);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      initPusher();
    });
  }

  // 监听生命周期变化
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    final me = context.read<MeProvider>().me;
    if (me == null) return;
    // 应用回到前台,检查连接状态,断开的话重新连接
    if (state == AppLifecycleState.resumed) {
      if (!_pusher.isConnected) {
        initPusher();
      }
    }
  }

  @override
  void dispose() {
    // 移除生命周期监听
    WidgetsBinding.instance.removeObserver(this);
    _streamSubscription.cancel();
    // 应用全局销毁时再调用dispose,普通页面销毁不要调用
    _pusher.dispose();
    super.dispose();
  }
}

2.3 双端系统适配

  • Android端:在AndroidManifest.xml中申请FOREGROUND_SERVICE权限,引导用户开启应用的「忽略电池优化」权限,避免后台被系统查杀
  • iOS端:在Xcode中开启Background Modes里的Remote notifications权限,后台期间的消息通过APNs推送触达用户

三、迁移至Socket.io方案

如果确认是Pusher SDK兼容问题,迁移Socket.io的逻辑和上述Pusher改造逻辑一致:

  1. 引入socket_io_client依赖
  2. 替换Pusher初始化逻辑为Socket.io初始化,开启autoConnect、reconnection配置
  3. 保留相同的生命周期监听、连接状态监听、事件转发逻辑即可,Socket.io自带的重连机制成熟度更高,适配成本很低。

内容的提问来源于stack exchange,提问作者user9662684

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 06:36:05