Flutter Future 复用问题

1,051 阅读3分钟

背景描述

Flutter 混合项目中,一个 Flutter 页面的下拉刷新,当出现网络连接错误的时候,再怎么刷新,也连不上了。

由于服务器在海外,所以怀疑网络不稳定,这种情况也正常,但诡异的是:

  1. 出现问题之后,即使重新开启科学上网,也无法连接上
  2. 退出页面,重新进入的时候,又可以连接了。
  3. 并且,当退出页面的时候,Native 部分的网络也正常。

使用了 Dio 作为网络库,并且搭配了 http2 adapter

分析1:定位问题

观察日志,当出现该问题时,出现以下异常

 HandshakeException: Connection terminated during handshake

首先,在 QA 同学的协助下,找到了可以复现的方式:在非科学上网的情况下,反复进出页面,不断刷新,会复现该问题。

尝试使用Flutter 单独项目跑起来,反复进入页面,发现无法复现。

比较这两者的不同:

正式项目里,反复重进页面,相当于 Flutter 模块不断重启,每次使用的 Dio 都是新的

而 Flutter 测试项目里,反复进页面,Dio 是复用的

尝试将这里改成每次创建 Dio 对象,可以出现连接中断问题,并且再次刷新,就又能连上了。

分析 2:尝试恢复

看起来是 Dio 对象的问题?一旦出现这个错误,将无法再使用?

在最外侧的 try-catch 中,尝试捕获该问题,并且重建 Dio 对象,看起来下次刷新时就可以修复了。

 Future<T?> get<T>(
    String path, {
    Map<String, dynamic>? queryParameters,
    Map<String, dynamic>? headers,
  }) async {
    try {
      // RequestOptions options = RequestOptions();
      Options options = Options();
      if (headers != null) {
        options.headers = headers;
      }

      Response<T> response = await client.dio.get<T>(
        path,
        queryParameters: queryParameters,
        options: options,
      );
      return response.data;
    } on DioError catch (e) {
      _handleLogout(e);
      if (e.type == DioErrorType.other && e.error is HandshakeException) {
        // 尝试重启
        client = MyNetworkClient(baseUrl: client.dio.options.baseUrl);
      }
      throw MyNetworkError(e);
    }
  }

但是为什么 Dio 对象就无法使用了呢?

此外,上述修复代码无法作用于共享了同样 client 对象的其他地方(client 是对 dio 的封装),需要搭配更多的修复代码

于是继续向下探究,是否有更合适的修复点,以及原因到底是什么

分析 3:继续追踪流程

找到抛出错误的地方:

github.com/flutterchin…

 @override
  Future<ClientTransportConnection> getConnection(
      RequestOptions options) async {
    if (_closed) {
      throw Exception(
          "Can't establish connection after [ConnectionManager] closed!");
    }
    var uri = options.uri;
    var domain = '${uri.host}:${uri.port}';
    var transportState = _transportsMap[domain];
    if (transportState == null) {
      var _initFuture = _connectFutures[domain];
      if (_initFuture == null) {
        _connectFutures[domain] = _initFuture = _connect(options);
      }
      
      // ↓↓↓↓↓ 这里 await initFuture 之后就会抛出异常 ↓↓↓↓↓
      transportState = await _initFuture;
      
      if (_forceClosed) {
        transportState.dispose();
      } else {
        _transportsMap[domain] = transportState;
        var _ = _connectFutures.remove(domain);
      }
    } else {
      // Check whether the connection is terminated, if it is, reconnecting.
      if (!transportState.transport.isOpen) {
        transportState.dispose();
        _transportsMap[domain] = transportState = await _connect(options);
      }
    }
    return transportState.activeTransport;
  }

观察 initFuture 是啥

 Future<_ClientTransportConnectionState> _connect(
      RequestOptions options) async {
    var uri = options.uri;
    var domain = '${uri.host}:${uri.port}';
    var clientConfig = ClientSetting();
    if (onClientCreate != null) {
      onClientCreate!(uri, clientConfig);
    }
    late SecureSocket socket;
    try {
      // Create socket
      
      // ↓↓↓↓↓ 从 connect 里抛出该异常 ↓↓↓↓↓
      socket = await SecureSocket.connect(
        uri.host,
        uri.port,
        timeout: options.connectTimeout > 0
            ? Duration(milliseconds: options.connectTimeout)
            : null,
        context: clientConfig.context,
        onBadCertificate: clientConfig.onBadCertificate,
        supportedProtocols: ['h2'],
      );
    } on SocketException catch (e) {
      if (e.osError == null) {
        if (e.message.contains('timed out')) {
          throw DioError(
            requestOptions: options,
            error: 'Connecting timed out [${options.connectTimeout}ms]',
            type: DioErrorType.connectTimeout,
          );
        }
      }
      rethrow;
    }
    // Config a ClientTransportConnection and save it
    var transport = ClientTransportConnection.viaSocket(socket);
    var _transportState = _ClientTransportConnectionState(transport);
    transport.onActiveStateChanged = (bool isActive) {
      _transportState.isActive = isActive;
      if (!isActive) {
        _transportState.latestIdleTimeStamp =
            DateTime.now().millisecondsSinceEpoch;
      }
    };
    //
    _transportState.delayClose(
      _closed ? 50 : _idleTimeout,
      () {
        _transportsMap.remove(domain);
        _transportState.transport.finish();
      },
    );
    return _transportState;
  }

当 connect 发生错误后,_connect 返回的 future,被保存在 _connectFutures 中,下次如果继续拿出来 await,会直接返回 exception

所以,需要在 SecureSocket.connect 对应的try-catch 中,捕获错误,并且执行 _connectFutures.remove(domain);

并且!

HandshakeException 不属于 SocketException,需要增加一个捕获的错误类型

分析 4:Future 的内部实现

之前没有重复使用过 Future,也就没有注意到这种写法的问题,

进一步往后分析,写一段简单的测试代码,如果一个 Future 执行过,并抛出了异常,那么去 await 他,之后会一直抛出异常

class Debug {
  Future<void> run() async {
    // await test1();
    // await test2();
    Future task = test3();
    try {
      await task;
    } catch (e) {
      print(e.toString());
    }
    for (int i = 0; i < 10; i++) {
      print("i: $i");
      try {
        await task;
      } catch (e) {
        print(e.toString());
      }
    }
  }
  
  Future<void> test3() async {
    var rnd = Random().nextInt(100);
    print(rnd);
    if (rnd > 50) {
      throw Exception("error!!!!!");
    }
  }
}

参考 Future 原理和使用的一些分析文章:

juejin.cn/post/684490…

zhuanlan.zhihu.com/p/450577968

Future 本身是有状态的,如果是 complete 状态(_stateValue 或者 _stateError),将会 cloneResult 并且直接进行下一步

image.png