【Flutter】异步操作

2,323 阅读5分钟

前言

线程和异步

平常开发中我们经常会用到异步操作比如网络请求、IO等耗时操作,其目的是避免阻塞主线程导致卡顿以及后续事件消费。

通常情况下像Java和C++中处理这些耗时操作是通过开辟新线程,在子线程中处理耗时操作最后将结果返回给主线程。而像Dart实现异步操作则是通过单线程+事件循环形式,每个事件在事件队列中排队执行并不阻塞线程执行最终结果以回调方式返回。所以我们要知道不是多线程不代表不能异步操作,两者没有必然关联。

阻塞调用

阻塞调用就是“一心一意”完成每一件事情,比如早晨起床你先刷牙再洗脸最后吃饭,洗脸必须等刷完牙才能执行,吃饭也必须等待洗脸完成后再执行。

非阻塞调用

非阻塞调用则就是“三心二意”完成每件事,比如你做一道番茄牛腩面,可以先把牛腩焯水煮同时你可以去处理番茄并把面条下锅,然后牛腩焯水差不多再将牛腩盛出。每件事情可以同时进行,不一定要等待某件事情完成后再去做其他事,只需要关注每件事情的结果再做其他处理。

Isolates和事件循环

Dart虽然是单线程语言但支持异步操作,因为Dart采用的是单线程+事件循环的工作模式处理异步操作。

Isolates

Isolates是所有Dart代码运行的地方,在机器中拥有一小块空间占有私有内存块单线程执行事件循环。不同于Java或是C++多线程共享同一内存,Dart每个Isolates都只有一个事件线程处理事务并相互隔离。

一般情况下一个Dart应用只有存在一个Isolates,当然你可以自己创建新Isolates去处理一些耗时操作,但两个Isolates之间还是相互隔离无法直接访问对方的任何资源。Isolates之间唯一交互方式是通过来回传递消息主要也是因为Dart是单线程事件循环也不存在线程锁等情况,若线程处在非繁忙情况就表示当前事件未发生变化。

事件循环

事件循环是Dart单线程实现异步操作的关键。实际上是运行在一个永不阻塞单线程处理一个事件循环,程序会不停的从事件队列中取出事件进行处理直到事件队列清空为止。

Future

Future可以理解为盒子,在没有打开之前你是拿不到也看不到里面会是什么东西。当你需要知道里面是什么时,执行then才会知道最终里面放着啥。 如下所示为Future异步操作延迟两秒返回数值2的完整过程。

Future.delayed(Duration(seconds: 2), () {
  return 2;
}).then((result) {
   //正常运行返回值为 2
}).catchError((error) {
  //若发生异常情况跑出异常
}).whenComplete(() {
 //不管执行成功还是跑出异常最终都会执行到这里
});

另外像Future.value(12)同样可以理解为“盒子”,其中存放的值我们无法直接获取它,只能异步执行也就是“拆盒子”的方式得到结果。

final myFuture = Future.value(12);
myFuture.then((value){
   _addLog("Hello World value $value");
 });

async/await

在执行Future异步操作属于“非阻塞”,如下代码中执行异步操作不会阻塞end日志打印。

() {
  _removeLog();
  _addLog("start");
  Future.delayed(Duration(seconds: 2), () => 'Large Latte')
      .then((result) {
    _addLog(result);
  });
  _addLog("end");
}

日志打印如下

start
end
Large Latte

同时Futrue异步请求可采用await“等待”返回异步结果,函数后面需要加上async表示为异步形式,另外await写法很好避免了地狱回调发生。例如在做异步操作后获取结果拿去做另一个异步操作,若异步操作链路很长就发生套娃现象这样的代码可读性大大减弱,善用await线性执行方式会很香的。同时await异步操作函数中代码执行过程中会出现“阻塞”,如下代码中在获取last之后才执行end日志,不过这只对函数内部是“阻塞”而对于外部调用者来说该函数本身还是异步操作。

前面的“等待”和“阻塞”是带引号,采用await和async代表整个函数是以异步形式执行并不会导致全局代码执行过程中等待该函数,要注意await和async还是异步操作。

() async {
    _removeLog();
    _addLog("start");
    String last = await Future.delayed(
        Duration(seconds: 2), () => 'Large Latte');
    _addLog(last);
    _addLog("end");
  },
)

日志打印如下

start
Large Latte
end

需要注意await是调用,所以在处理异常情况和回调方式有些不同,需要在外层嵌套try/catch避免Future异步操作“阻塞”导致后续代码无法执行。

try {
  String last =
      await Future.delayed(Duration(seconds: 2), () {
    throw "throw error";
  }).whenComplete(() {
   
  });
} catch (e) {
  _addLog("catch $e");
}

wait

等待多个异步请求集合结果,例如下面代码中两个异步函数执行等待两秒后统一将结果集合返回。最终结果返回需要等待最后一个异步操作执行完后统一返回。

Future.wait([
  Future.delayed(Duration(seconds: 1), () => 1),
  Future.delayed(Duration(seconds: 2), () => 2),
]).then((values) {
  _addLog("values ${values[0]} ${values[1]}");
});

delayed

Future的延迟操作中采用了Timer计时器,等待计时器结束后返回结果值。

factory Future.delayed(Duration duration, [FutureOr<T> computation()]) {
  _Future<T> result = new _Future<T>();
  new Timer(duration, () {
    if (computation == null) {
      result._complete(null);
    } else {
      try {
        result._complete(computation());
      } catch (e, s) {
        _completeWithErrorCallback(result, e, s);
      }
    }
  });
  return result;
}

Stream

Stream和Future同样是异步操作,不同于Future异步操作单个值形式,Stream代表着异步操作数据流。不同于await/async异步形式,Stream数据流准备以async*/yield形式展示。

Stream<int> countStream(int to) async* {
    for (int i = 1; i <= to; i++) {
      yield i;
    }
 }
() {
  countStream(10).listen((data) {
     ///结果返回
     throw "Error";
   }, onDone: () {
     ///相当于finally
   }, onError: () {
     ///抛出异常的地方
   });
} 

默认Stream只支持单个订阅监听,若需要支持多个订阅监听需要通过asBroadcastStream转变为广播订阅。

() {
  final stream = countStream(10).asBroadcastStream();
  stream.listen(
    (data) => _addLog("Stream1 $data"),
  );
  stream.listen(
    (data) => _addLog("Stream2 $data"),
  );
},

StreamController

StreamController是创建属于你自己的Streams的高级用法。通过StreamController你可以很好的控制数据流进出操作。

/// 创建一个StreamController对象
class NumberCreator {
  ///初始化对象中Stream数据流
  NumberCreator() {
    for (int i = 0; i < 10; i++) {
      _controller.sink.add(_count);
      _count++;
    }
  }

  var _count = 1;
  ///创建StreamController流控制器
  StreamController<int> _controller = StreamController<int>();

  Stream<int> get stream => _controller.stream;
}
///(1)创建流控制器
numberCreator = NumberCreator();
///(2)创建流监听监听
subscriptionListen =
    numberCreator.stream.listen((value) {
});
///(3)向流控制器输入数据
numberCreator._controller.add(100);
///暂停监听
subscriptionListen.pause();
///重启监听
subscriptionListen.resume();
///移除流监听
subscriptionListen.cancel();
  • 监听启动后监听器可获取StreamController的数据流
  • StreamController可随时push新数据
  • 监听暂停后监听器不会获取StreamController数据流,同时StreamController新增数据将暂存
  • 但监听器重新resume之后可获取StreamController暂存未消费数据流

StreamBuilder

若是在UI组件直接使用StreamBuilder可以快速实现根据数据流展示不同UI内容。StreamBuilder组件内部设置内部监听订阅外部stream数据流。

void _subscribe() {
    if (widget.stream != null) {
      _subscription = widget.stream.listen((T data) {
        setState(() {
          _summary = widget.afterData(_summary, data);
        });
      }, onError: (Object error) {
        setState(() {
          _summary = widget.afterError(_summary, error);
        });
      }, onDone: () {
        setState(() {
          _summary = widget.afterDone(_summary);
        });
      });
      _summary = widget.afterConnected(_summary);
    }
 }

通过stream传递数据在builder回调获取到snapshot数据在UI上做展示。

StreamBuilder<int>(
  builder: (context, data) {
    return Text(data.data.toString());
  },
  initialData: 10000,
  stream: numberCreator?.stream,
),

🚀完整代码看这里🚀 🚀完整代码看这里🚀

参考