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

Dart Isolate监听无响应问题排查与解决求助

问题解决:FlutterDownloader后台回调无法传递数据到UI Isolate

针对你遇到的后台downloadCallback正常执行但UI侧_port.listen()收不到数据的问题,以下是具体修复方案:

核心问题定位

主要原因是跨Isolate消息序列化失败,同时存在端口注册时机、资源清理不规范等辅助问题。


分步修复

1. 修复枚举序列化问题

DownloadTaskStatus是枚举类型,直接传递实例会导致跨Isolate序列化失败,需传递枚举的value值,再在UI侧转换回枚举:

修改后台回调:

@pragma('vm:entry-point')
static void downloadCallback(
    String id,
    DownloadTaskStatus status,
    int progress,
    ) {
  simplelog.i('Callback on background isolate: '
      'task ($id) is in status ($status) and progress ($progress)');

  // 传递枚举value而非实例
  IsolateNameServer.lookupPortByName('downloader_send_port')
      ?.send([id, status.value, progress]);
}

修改UI侧监听逻辑:

_port.listen((dynamic data) {
  simplelog.i('in listen');
  final String _id = data[0];
  // 从value转换回DownloadTaskStatus枚举
  final DownloadTaskStatus _status = DownloadTaskStatus(data[1]);
  final int _progress = data[2];
  // ... 后续逻辑保持不变
});

2. 调整回调注册时机

确保端口绑定完成后再注册FlutterDownloader回调,避免后台Isolate找不到端口:

@override
void initState() {
  super.initState();
  _bindBackgroundIsolate();
  // 延迟注册回调,确保端口已完成注册
  WidgetsBinding.instance.addPostFrameCallback((_) {
    FlutterDownloader.registerCallback(downloadCallback);
  });
}

3. 避免端口注册死循环

原代码中注册失败时递归调用_bindBackgroundIsolate可能导致无限循环,添加重试限制:

void _bindBackgroundIsolate() {
  // 先检查端口是否已注册,避免重复操作
  if (IsolateNameServer.lookupPortByName('downloader_send_port') != null) {
    simplelog.i('Port already registered');
    return;
  }

  final isSuccess = IsolateNameServer.registerPortWithName(
      _port.sendPort, 'downloader_send_port');
  simplelog.i('regPort: $isSuccess');

  if (!isSuccess) {
    simplelog.e('Failed to register port, retrying once');
    _unbindBackgroundIsolate();
    // 仅重试一次
    IsolateNameServer.registerPortWithName(
        _port.sendPort, 'downloader_send_port');
    return;
  }

  // ... 监听逻辑保持不变
}

4. 规范资源清理

为监听添加订阅对象,在页面销毁时取消订阅,避免内存泄漏:

// 在State类中添加订阅对象
StreamSubscription? _portSubscription;

// 修改监听逻辑
_portSubscription = _port.listen((dynamic data) {
  // ... 原监听逻辑
});

// 在dispose中清理
@override
void dispose() {
  _portSubscription?.cancel();
  _unbindBackgroundIsolate();
  super.dispose();
}

5. 修正路径初始化时机

原代码中downloadsPath作为类级getter可能在初始化前被调用,改为在initState中初始化:

late String downloadsPath;

@override
void initState() {
  super.initState();
  downloadsPath = storage.read('downloadsPath') ?? AndroidPathProvider.downloadsPath;
  // ... 其他初始化逻辑
}

完整修正代码

import 'dart:isolate';
import 'dart:ui';

import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:flutter/widgets.dart';

class SettingsPage extends StatefulWidget with WidgetsBindingObserver {
  SettingsPage({Key? key}) : super(key: key);

  @override
  State<SettingsPage> createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
  final ReceivePort _port = ReceivePort();
  int uploadProgress = 0;
  DownloadTaskStatus uploadStatus = DownloadTaskStatus(-1);
  String identifier = '';
  late String downloadsPath;
  StreamSubscription? _portSubscription;

  @override
  void initState() {
    super.initState();
    downloadsPath = storage.read('downloadsPath') ?? AndroidPathProvider.downloadsPath;
    _bindBackgroundIsolate();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      FlutterDownloader.registerCallback(downloadCallback);
    });
  }
 
  @override
  void dispose() {
    _portSubscription?.cancel();
    _unbindBackgroundIsolate();
    super.dispose();
  }

  void _bindBackgroundIsolate() {
    if (IsolateNameServer.lookupPortByName('downloader_send_port') != null) {
      simplelog.i('Port already registered');
      return;
    }

    final isSuccess = IsolateNameServer.registerPortWithName(
        _port.sendPort, 'downloader_send_port');
    simplelog.i('regPort: $isSuccess');

    if (!isSuccess) {
      simplelog.e('Failed to register port, retrying once');
      _unbindBackgroundIsolate();
      IsolateNameServer.registerPortWithName(
          _port.sendPort, 'downloader_send_port');
      return;
    }

    simplelog.i('before listen');

    _portSubscription = _port.listen((dynamic data) {
      simplelog.i('in listen');
      final String _id = data[0];
      final DownloadTaskStatus _status = DownloadTaskStatus(data[1]);
      final int _progress = data[2];

      simplelog.i(
        'Callback on UI isolate: '
        'task ($_id) is in status ($_status) and process ($_progress)',
      );

      setState(() {
        uploadProgress = _progress;
        uploadStatus = _status;
        simplelog.i('inside setState');
      });

      simplelog.i(
          'uploadProgress = $uploadProgress \n uploadStatus = $uploadStatus \n uploadStatus.value = ${uploadStatus.value}');
      if (uploadProgress == 100 &&
          uploadStatus == DownloadTaskStatus.complete &&
          uploadStatus.value == 3) {
        try {
          OpenFilex.open('$downloadsPath/${Consts.apkName}');
        } catch (e) {
          simplelog.e('Cannot open file ${Consts.apkName}! \n $e');
        }
      }
    }, onError: (err) {
      simplelog.e('ISOLATE LISTEN ERROR: $err');
    }, onDone: () {
      simplelog.i('onDone listen');
    });
  }

  void _unbindBackgroundIsolate() {
    IsolateNameServer.removePortNameMapping('downloader_send_port');
  }

  @pragma('vm:entry-point')
  static void downloadCallback(
      String id,
      DownloadTaskStatus status,
      int progress,
      ) {
    simplelog.i('Callback on background isolate: '
        'task ($id) is in status ($status) and progress ($progress)');

    IsolateNameServer.lookupPortByName('downloader_send_port')
        ?.send([id, status.value, progress]);
  }
}

额外排查点

  • 测试时可将simplelog替换为print,避免日志库在后台Isolate中无法输出。
  • 检查是否有其他代码修改了IsolateNameServer的端口映射,导致端口被覆盖。
  • 验证下载任务ID与UI侧保存的identifier是否一致,确保回调传递的是当前页面关注的任务。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 19:35:46