乐趣区

flutter入门线程异步声明式UI

关于 flutter 的背景、体系结构、横向对比等,建议阅读淘宝的一篇文章,大比拼 | 下一代高性能跨平台 UI 渲染引擎,人家是真的厉害。

这里就不多贴这些宏观的简介了。本文主要从客户端开发的角度看三个小点:线程、异步、声明式 UI,都是 Flutter 跟正常的客户端开发有一定区别的地方。

线程模型

线程模型
线程模型这块,大多数文章对初学者来讲都有点不清不楚的,这里详细总结一下。

首先,在上层 flutter APP 里,我们用 dart 语言开发,这个层面,是没有线程的概念的,取而代之的是 dart 提供的类似线程的 isolate。isolate 简单来讲就是个受限制的线程,isolate 之间只能通过一种叫 port 的消息机制通信,不能共享内存。除此之外跟线程是一样的。
dart vm 默认提供了一个 root isolate,有耗时操作需要执行时,可以 new 出新的 isolate 执行。
Flutter engine 这个层面,有四个 Runner 各司其职,这里的 Runner 其实就是线程,不过这四个 Runner 是由 Engine 和 Native 之间的那个嵌入层去赋值的,engine 层只会使用这四个 Runner,不会创建新的线程。默认地,Platform Runner 和 Native 的主线程是同一个线程。
回头看 dart 的 root isolate,它跟 engine 层的 UI Runner 是绑定的,即,它们两个是同一个线程。

整体看一下,会发现一些特别的东西。对 Flutter App 来讲,root isolate 基本上可以理解为主线程,同时它也是 UI 线程。但是,它不是 Native 层面的主线程,在 Native 看来,它只是个子线程。

dart 异步编程

callback

对异步编程而言,客户端开发最熟悉的可能是 callback 语法,当然很多时候也会使用 delegate。dart 的 callback 语法如下:

Timer.run(() => print('hi!'));

不过虽然 dart 也可以用 callback,但是更多的时候,会使用 Future/async/await 这套语法来执行异步任务。

Future/async/await

Future<Response> dateRequest() async {
  String url = 'https://www.baidu.com';
  Client client = Client();
  Future<Response> response = client.get(requestURL);
  return response;
}

Future<String> loadData() async {Response response = await dataRequest();
  return response.body;
}

简单看一下这个小例子,client.get()是个异步的网络请求,它可以直接返回一个 Future<Response> 的对象,这个名字很有意思,它的意思是,我以后会给你个 Response 类型的对象的,但是现在,只是个空头支票(Future)。

之后,可以使用 await 关键字加上这个 Future,当前调用就会停在这里,直到这个Future 对象返回才会继续向下执行。基本原理是,把当前上下文存到堆内存;当 Future 返回时,会产生一个 event 进入 eventloop(基本上是个语言都有这么个玩意儿,可以参考 Dart 与消息循环机制),这个 event 会触发进入之前的上下文继续执行。

可以看到,这里的写法很像同步的写法,但是它是不会阻塞当前线程的,原理上面已经简单解释了。目前,async/await 这种异步语法,是公认的异步语法的最佳方案。前端和安卓的 kotlin 已经比较广泛地使用了,而 iOS 还没跟得上时代。

单线程语言 & isolate

前面讲 Flutter 线程模型时,已经提到了 isolate。它在底层其实就是个线程,但是 dart vm 限制了 isolate 的能力,使得 isolate 之间不能直接共享内存,只能通过 Port 机制收发消息。
看一下代码

void main() async{runApp(MyApp());
  
  //asyncFibonacci 函数里会创建一个 isolate,并返回运行结果
  print(await asyncFibonacci(20));
}

// 这里以计算斐波那契数列为例,返回的值是 Future,因为是异步的
Future<dynamic> asyncFibonacci(int n) async{final response = new ReceivePort();
  await Isolate.spawn(isolateTask,response.sendPort);
  final sendPort = await response.first as SendPort;
  final answer = new ReceivePort();
  sendPort.send([n,answer.sendPort]);
  return answer.first;
}
// 创建 isolate 必须要的参数
void isolateTask(SendPort initialReplyTo){final port = new ReceivePort();
  // 绑定
  initialReplyTo.send(port.sendPort);
  // 监听
  port.listen((message){
    // 获取数据并解析
    final data = message[0] as int;
    final send = message[1] as SendPort;
    // 返回结果
    send.send(syncFibonacci(data));
  });
}

int syncFibonacci(int n){return n < 2 ? n : syncFibonacci(n-2) + syncFibonacci(n-1);
}

Port 分为 ReceivePort 和 SendPort,这两者是成对出现的,在新建一个 isolate 的时候,可以传入一个 sendPort 用于 isolate 向主线程发消息,如果主线程想往子线程发消息呢 … 就只能让子线程 new 出一对 port 把 sendport 发过来才能用 …

语法上是很啰嗦了,所幸 Flutter 给我们封装了便捷的 compute 函数,可以参考深入了解 Flutter 的 isolate(4) — 使用 Compute 写 isolates,由于只是上层封装,这里就不具体展开了。

到这里我们基本上明白了,isolate 就是个削弱版的线程,用起来麻烦一点,另外就是由于不共享内存,port 发送数据时是 copy 的,如果有大块内存真的要 copy 多份,可能会有比较大的内存问题。

但是,官方明确说明,dart 是个单线程语言,这怎么理解呢?还是要回到 isolate 和线程的区别。由于 isolate 之间是不共享内存的,它们其实基本上是完全隔离的。隔离就是这里的关键,从上层代码来看,是的,我的代码开了好几个线程,但是,从执行逻辑上,每个 isolate 直接是相互隔离的,对每个 isolate 内的逻辑来讲,它就是单线程的。

声明式 UI

声明式 UI 与响应式 UI 是对应的概念,考虑一下 iOS/android 的 UI 实现。
iOS 是很纯粹的命令式,new view,addsubview,new view,addsubview,这样搞。
安卓呢,算是半命令式吧,xml 声明了 UI,这是声明式的部分;但程序运行时如果要修改某个 view,仍是取到这个 view,再去命令式地修改。

下面来看看 flutter 的框架

flutter 的 UI 框架吸取了 react 的理念,即 UI 是关于状态的函数。
具体看一下 demo

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {setState(() {_counter++;});
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title),
        leading: IconButton(icon:Icon(Icons.arrow_back),
          onPressed:() => SystemNavigator.pop(),
        )
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:',),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

这个是官方的 helloworld demo。每个组件,会有个 build 函数,这里会返回一个能够完整描述 UI 的对象结构。每当数据改变时,就重新调用 build 函数,返回新的结构。如何高效渲染,就是框架去做的事情了。

通过这种方式,不管是 UI 的初始布局结构,还是后面的修改,都是 build 函数返回的对象结构去声明的,完整的声明式 UI 由此而来。

UI 开发的最佳实践是怎么样的,一直以来都充满争议。但近几年,React -> Flutter -> SwiftUI,都使用了声明式的 UI 编程范式,可以看到头部公司基本上达成了共识,目前阶段,这就是最佳实践。

退出移动版