带你深入理解Flutter及Dart单线程模型

2,616 阅读4分钟

前言

大家好,我是未央歌,一个默默无闻的移动开发搬砖者~

众所周知,Java 是一种多线程语言,适量并合适地使用多线程,会极大提高资源利用率和运行效率,但缺点也明显,比如开启过多的线程会导致资源和性能的消耗过大以及多线程共享内存容易死锁

而 Dart 则是一种单线程语言,单线程语言就意味着代码执行顺序是有序的,下面结合一个demo带大家深入了解单线程模型。

demo 示例

点击 APP 右下角的刷新按钮,会调用如下方法,读取一个约 2M 大小的 json 文件。

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
}

如下图所示,点击刷新按钮之后,中间的 loading 会卡一下。很多同学一看这个代码就知道,肯定会卡,解析一个 2M 的文件,而且是同步解析,主页面肯定是会卡的。 那如果我换成异步解析呢?还卡不卡?大家可以脑海中思考下这个问题。

异步解析

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  // 异步解析
  Future(() {
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
  }).then((value) {});
}

大家可以看到,我已经放在异步里解析了,为什么还是会卡呢?大家可以先思考下这个问题。

前面已经提到了 Dart 是一种单线程语言,单线程语言就意味着代码执行顺序是有序的。当然 Dart 也是支持异步的。这两点其实并不冲突。

Dart 线程解析

我们来看看 Dart 的线程,当我们 main() 方法启动之后,Dart已经开启了一个线程,这个线程的名字就叫 Isolate。每一个 Isolate 线程都包含了图示的两个队列,一个 Microtask queue,一个 Event queue

如图,Isolate 线程会优先执行 Microtask queue 里的事件,当 Microtask queue 里的事件变成空了,才会去执行 Event queue 里的事件。如果正在执行 Microtask queue 里的事件,那么 Event queue 里的事件就会被阻塞,就会导致渲染、手势响应等都得不到响应(绘制图形,处理鼠标点击,处理文件IO等都是在 Event Queue 里完成)。

所以为了保证功能正常使用不卡顿,尽量少在 Microtask queue 做事情,可以放在 Event queue 做

为什么单线程可以做一个异步操作呢?

  • 因为 APP 只有在你滑动或者点击操作的时候才会响应事件。没有操作的时候进入等待时间,两个队列里都是空的。这个时间正是可以进行异步操作的,所以基于这个特点,单线程模型可以在等待过程中做一些异步操作,因为等待的过程并不是阻塞的,所以给我们的感觉就像同时在做多件事情,但自始至终只有一个线程在处理事情。

Future

当方法加上 async 关键字,就代表这个方法开启了一个异步操作,如果这个方法有返回值,就必须要返回一个 Future。

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  // 异步解析
  Future(() {
    ...
  }).then((value) {});
}

一个 Future 异步任务的执行,相对简单。在我们声明一个 Future 之后,Dart 会将异步里的代码函数体放在 Event queue 里执行然后返回。这里注意下,Future 和 then 是放在同一个 Event queue 里的。

假设,我执行 Future 代码之后没有立即执行 then 方法,而是等 Future 执行之后5秒,才调用 then 方法,这时候还是放在同一个 Event queue 里吗?显然是不可能的,我们看一下源码是怎么实现的。

Future<R> then<R>(FutureOr<R> f(T value), {Function? onError}) {
  ...
  _addListener(new _FutureListener<T, R>.then(result, f, onError));
  return result;
}

bool get _mayAddListener => _state <= (_statePendingComplete | _stateIgnoreError);

void _addListener(_FutureListener listener) {
  assert(listener._nextListener == null);
  if (_mayAddListener) {
    // 待完成
    listener._nextListener = _resultOrListeners;
    _resultOrListeners = listener;
  } else {
    // 已完成
    ...
    _zone.scheduleMicrotask(() {
      _propagateToListeners(this, listener);
    });
  }
}

可以看到 then 方法里有一个监听,Future 执行之后5秒才调用,很明显是已完成状态,走 else 那里的 scheduleMicrotask() 方法,就是说把 then 里面的方法放到 Microtask queue 里。

Future 为何卡顿

再来说一下刚刚的问题,我已经放在异步里解析了,为什么还是会卡呢?

其实很简单,Future 里的代码可能需要执行10s,也就是 Event queue 需要10s才能执行完。那这个10s内其他代码肯定就无法执行了。所以 Future 里的代码执行时间过长,还是会卡 UI 的。

以 Android 为例,Android的刷新频率是60帧/秒,Android系统中每隔16.6ms会发送一次 VSYNC(同步)信号,触发UI的渲染。所以我们就要考虑下,一旦代码执行时间超过16.6ms,到底应不应该放在 Future 里执行?

这时候是不是有同学有疑问,我网络请求也是用 Future 写的,为什么就不卡呢?

这个大家就需要注意一下,网络请求不是放在 Dart 层面执行的,它是由操作系统提供的异步线程去执行的,当这个异步执行完系统又返回给 Dart。所以即使 http 请求需要耗时十几秒,也不会感到卡顿。

compute

既然 Future 执行也会卡顿,那要怎么去优化呢?这时候我们可以开一个线程操作,Flutter 为我们封装好了一个 compute()方法,这个方法可以为我们开一个线程。我们用这个方法来优化一下代码,然后再看下执行效果。

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  var result = compute(parse,jsonStr);
}

static VideoListModel parse(String jsonStr){
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  return VideoListModel.fromJson(json.decode(jsonStr));
}

可以看到此时点击刷新按钮,已经不再卡顿了。遇到一些耗时的操作,这确实是一种比较好的解决方式。

我们再看看 DefaultAssetBundle.of(context).loadString("assets/list.json") 方法里面是怎么执行的。

Future<String> loadString(String key, { bool cache = true }) async {
  final ByteData data = await load(key);
  if (data == null)
    throw FlutterError('Unable to load asset: $key');
  // 50 KB of data should take 2-3 ms to parse on a Moto G4, and about 400 μs
  // on a Pixel 4.
  if (data.lengthInBytes < 50 * 1024) {
    return utf8.decode(data.buffer.asUint8List());
  }
  // For strings larger than 50 KB, run the computation in an isolate to
  // avoid causing main thread jank.
  return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"');
}

从官方源码可以看到,当文件的大小超过 50kb 时,也是采用 compute() 方法开一个线程去操作的。

多线程机制

Dart 作为一个单线程语言,虽然提供了多线程的机制,但是在多线程的资源是隔离的,两个线程之间资源是不互通的

Dart 的多线程数据交互需要从 A 线程传给 B 线程,再由 B 线程返回给 A 线程。而像 Android 在主线程开一个子线程,子线程可以直接拿主线程的数据,而不用让主线程传给子线程。

总结

  • Future 适合耗时小于 16ms 的操作
  • 可以通过 compute() 进行耗时操作
  • Dart 是单线程原因,但也支持多线程,但是线程间数据不互通

最后

如果你对 Flutter 感兴趣,可以看看我的专栏:

感谢大家的支持,码字实在不易,其中如若有错误,望指出,记得点赞关注加收藏哦 ~