flutter入门:线程,异步,声明式UI

更新于 2019-11-25  约 11 分钟

关于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其实是内核级线程的封装,在系统内核层面就是多线程的。
我觉得这句话可能从理论模式上理解会比较好。
并发编程长期以来有两种范式,一种是基于共享内存的,主要是多线程编程;一种是基于消息的,如Actor、CSP模型。从这个角度看,isolate其实是消息驱动的并发编程,算是CSP模型的简化,跟多线程编程是完全不同的并发编程范式。
因此说dart是个单线程语言也是说得通的。

声明式UI

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

下面来看看flutter的框架
Declarative UI
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编程范式,可以看到头部公司基本上达成了共识,目前阶段,这就是最佳实践。

阅读 816更新于 2019-11-25

推荐阅读
二师兄的博客
用户专栏

个人博客,希望这次能多写点

3 人关注
41 篇文章
专栏主页
目录