关于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编程范式,可以看到头部公司基本上达成了共识,目前阶段,这就是最佳实践。