首先说一下,为什么要关心iOS和Flutter的区别问题。因为移动端开发的业务逻辑设计模式等是一致的,区别可能只在于使用的语言不同,实现逻辑的风格不同而已。所以这里我们先分析一下iOS和Flutter的区别到底有哪些,有利于我们更快地去入门。
移动端开发首先要关注的一点肯定要了解一个页面加载的生命周期,就像了解iOS的viewcontroller的生命周期在UI绘制的场景中有多么重要:
iOS的设计初衷就是MVC,所以iOS中的核心是Controller。每个Controller都有自己的生命周期:
//类的初始化方法
+ (void)initialize;
//对象初始化方法
- (instancetype)init;
//从归档初始化
- (instancetype)initWithCoder:(NSCoder *)coder;
//加载视图
-(void)loadView;
//将要加载视图
- (void)viewDidLoad;
//将要布局子视图
-(void)viewWillLayoutSubviews;
//已经布局子视图
-(void)viewDidLayoutSubviews;
//内存警告
- (void)didReceiveMemoryWarning;
//已经展示
-(void)viewDidAppear:(BOOL)animated;
//将要展示
-(void)viewWillAppear:(BOOL)animated;
//将要消失
-(void)viewWillDisappear:(BOOL)animated;
//已经消失
-(void)viewDidDisappear:(BOOL)animated;
//被释放
-(void)dealloc;
这一点flutter则有所不同,flutter页面加载的核心则是build一棵渲染树的过程。
如图所示大体上它的生命周期可以分为三个过程:初始化,状态改变,销毁。每一次build都是页面的一次加载。
didUpdateWidget: Flutter中的Widget分为两种:stateful(状态可变)和stateless(状态不可变)。如果是stateful的widget需要实现setState方法。当调用了 setState 将 Widget 的状态被改变时 didUpdateWidget 会被调用,Flutter 会创建一个新的 Widget 来绑定这个 State,并在这个方法中传递旧的 Widget ,因此如果你想比对新旧 Widget 并且对 State 做一些调整,你可以用它,另外如果你的某些 Widget 上涉及到 controller 的变更,要么一定要在这个回调方法中移除旧的 controller 并创建新的 controller 监听。
dispose:某些情况下你的 Widget 被释放了,一个很经典的例子是 Navigator.pop 被调用,如果被释放的 Widget 中有一些监听或持久化的变量,你需要在 dispose 中进行释放。很重要的一个应用是在 Bloc 或 Stream 时在这个回调方法中去关闭 Stream。
可参考博客:https://segmentfault.com/a/1190000015211309
iOS中的APP的生命周期在appdelegate里设置,这里不多说。
而在flutter中如果我们想监听 App 级别的生命周期,可以通过向Binding 中添加一个???? Observer,同时要实现didChangeAppLifecycleState来监听指定事件的到来,并且最后还需要在 dispose 回调方法中移除这个监听。但限制是它在iOS平台智能监听三种状态。
class LifeCycleDemoState extends State with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.inactive:
print('Application Lifecycle inactive');
break;
case AppLifecycleState.paused:
print('Application Lifecycle paused');
break;
case AppLifecycleState.resumed:
print('Application Lifecycle resumed');
break;
default:
print('Application Lifecycle other');
}
}
@override
void dispose(){
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
详细使用源码看这里:https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/binding.dart
Flutter的布局首先要建立一棵Widget树(分析一下UI图建立一棵控件树),根据树的结构层层嵌套Widget控件。margin和padding等的约束布局设置则有点类似css。
iOS:UIView可变,UIView发生改变本质上是间接调用(官方建议不要显式调用,因为这开销很大)了LayoutSubviews方法进行布局上的重构,drawRect进行显示上的重构,updateConstrains进行约束上的重构。三者的更新会在每次RunLoop之后进行update cycle操作(也可以调用layoutIfNeeded等方法进行立刻更新)。(推荐一篇讲述布局不错的文章:https://juejin.im/post/5a951c655188257a804abf94)
Flutter:Widget不可变,也正是由于不可变性,使得Widget比起UIView来说更轻量,因为它不是控件,只是UI的描述。
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State {
// Default placeholder text
String textToShow = "Flutter";
void _updateText() {
setState(() {
// update the text
textToShow = "Text is changed!";
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My App"),
),
body: Center(child: Text(textToShow)),
floatingActionButton: FloatingActionButton(
onPressed: _updateText,
child: Icon(Icons.update),
),
);
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(300, 600, 80, 40);
[button setTitle:@"点击" forState:UIControlStateNormal];
[button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
self.textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 300, 200, 60)];
self.textField.text = @"iOS";
[self.view addSubview:self.textField];
}
-(void) onClick
{
self.textField.text = @"text has changed!";
}
父子视图的移除方面,比如说举个常见的例子,一个bool值的改变导致两个视图的切换,iOS需要在父view中调用addSubview()或在子view中调用removeFromSuperView() 来动态添加或移除子view,如果涉及到一些传值问题可能还需要通过观察者模式来观察bool值的更新。而在flutter中则需要向父widget中传入一个返回widget的函数,并用bool来控制子widget的创建。
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State {
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
_getToggleChild() {
if (toggle) {
return Text('View One');
} else {
return CupertinoButton(
onPressed: () {},
child: Text('View Two'),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: _getToggleChild(),
),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Text',
child: Icon(Icons.update),
),
);
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(350, 750, 50, 30);
[button setTitle:@"切换" forState:UIControlStateNormal];
[button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
self.textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 300, 200, 60)];
self.textField.text = @"View 1";
[self.view addSubview: self.textField];
self.button2 = [UIButton buttonWithType:UIButtonTypeSystem];
self.button2.frame = CGRectMake(100, 300, 200, 60);
[self.button2 setTitle:@"View 2" forState:UIControlStateNormal];
}
-(void) onClick
{
if (self.textField.superview) {
[self.textField removeFromSuperview];
[self.view addSubview:self.button2];
}
else
{
[self.button2 removeFromSuperview];
[self.view addSubview:self.textField];
}
}
Flutter中没有专门用来管理视图而且类似那种和View一对一的Controller类。有类似的Scaffold,其包含控制器的appBar,也可以通过body设置一个widget来做其视图。
iOS有UINavigationController栈进行push,pop操作,其并不负责显示,而是负责各个页面跳转。或者使用模态视图。
同时注意iOS通过navigationController导航时要记得添加NavigationController,可在stroyboard设置,如果代码的话添加如下:
ViewController *vc = [ViewController new];
UINavigationController *navigation = [[UINavigationController alloc] initWithRootViewController:vc];
self.window.rootViewController = navigation;
[self.window makeKeyWindow];
// -------- ViewController --------
-(NextViewController *)next
{
if (!_next)
{
_next = [NextViewController new];
}
return _next;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(350, 750, 50, 30);
[button setTitle:@"切换" forState:UIControlStateNormal];
[button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
-(void) onClick
{
[self.navigationController pushViewController:self.next animated:YES];
// [self presentViewController:self.next animated:YES completion:nil];
}
// -------- NextViewController --------
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// Do any additional setup after loading the view.
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(350, 750, 50, 30);
[button setTitle:@"返回" forState:UIControlStateNormal];
[button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
-(void) onClick
{
[self.navigationController popViewControllerAnimated:YES];
// [self dismissViewControllerAnimated:YES completion:nil];
}
Flutter中可以将MaterialApp理解为iOS的导航控制器,其包含一个navigationBar以及导航栈,这点和iOS是一样的。
Flutter中使用了 Navigator
和 Routes
。一个路由是 App 中“屏幕”或“页面”的抽象,而一个 Navigator 是管理多个路由的 widget。可以粗略地把一个路由对应到一个 UIViewController
。Navigator 的工作原理和 iOS 中 UINavigationController
非常相似,当你想跳转到新页面或者从新页面返回时,它可以 push()
和 pop()
路由。
在页面之间跳转,有如下选择:
Map
。(MaterialApp)下面是构建一个 Map 的例子:
void main() {
runApp(MaterialApp(
home: MyAppHome(), // becomes the route named '/'
routes: {
'/a': (BuildContext context) => MyPage(title: 'page A'),
'/b': (BuildContext context) => MyPage(title: 'page B'),
'/c': (BuildContext context) => MyPage(title: 'page C'),
},
));
}
通过把路由的名字 push
给一个 Navigator
来跳转:
Navigator.of(context).pushNamed('/b');
iOS中页面传值正向直接通过属性传输即可,反向的话可以通过Block,delegate,通知等方式进行传值。
而在Flutter中反向传值就简单了很多,举个例子,要跳转到“位置”路由来让用户选择一个地点,可能要这么做:
Map coordinates = await Navigator.of(context).pushNamed('/location');
之后,在 location 路由中,一旦用户选择了地点,携带结果一起 pop()
出栈:
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
RunLoop
VS EventLoop
说起Flutter的多线程前先谈一下Flutter中的Event Loop。我们都知道前端开发框架大都是事件驱动的。意味着程序中必然存在事件循环和事件队列。事件循环会不断地从事件队列中获取和处理各种事件。
iOS:iOS中的类似机制是RunLoop,可以通过一个RunLoopObserver来实现对RunLoop的观察,当遇到Source0,Source1或者Timer等事件会进行处理。下面图可以很清晰的理解整个过程。(关于RunLoop的讲解推荐一篇很详细的文章:https://juejin.im/post/5add46606fb9a07abf721d1d)
Flutter:Flutter中的Event Loop和JavaScript的基本一样。循环中有两个队列。一个是微任务队列(MicroTask queue),一个是事件队列(Event queue)。这里类似iOS中存在set里面的Source0和Source1。
isolate
内部添加事件,事件的优先级比event queue
高。两个队列是有优先级的,当isolate
开始执行后,会先处理microtask
的事件,当microtask
队列中没有事件后,才会处理event
队列中的事件,并按照这个顺序反复执行。当执行microtask
的事件时会阻塞event
队列,这会导致渲染响应手势等event
事件响应延迟。为保证渲染和手势响应,所以应尽量将耗时操作放到event
队列中。
Flutter中的多线程和异步是比较难理解的一块。先看两个语法糖:
Future:
学过js的童鞋应该很容易理解这块了,Future就是js里的Promise,曾经js里面延时只能层层回调,这样很容易造成回调地狱。所以应运而生了Promise(Future),它是一个链式操作,可以通过追加then方法进行多层处理。
async/await:用法完全沿用自js。
多协程:
我们拆开来看,首先对于异步操作这块,flutter基本上是沿用ES6的async和await这些异步语法糖以及Future。这里涉及到一个和传统意义不一样的概念——协程(https://www.itcodemonkey.com/article/4620.html这篇漫画讲的比较生动,https://www.zhihu.com/question/308641794讲解为什么协程比线程要好),最早接触是在python里有遇到过,在Flutter中,执行到async则表示进入一个协程,会同步执行async的代码块。当执行到await时,则表示有任务需要等待,CPU则去调度执行其他IO。过一段时间CPU会轮询一次查看某个协程是否任务已经处理完成,有返回结果可以被继续执行,如果可以被继续执行的话,则会沿着上次离开时指针指向的位置继续执行。也就是await标志的位置。
iOS中有没有类似的实现呢,其实是有的,个人感觉是串行队列中的dispatch_async
操作,遇到耗时操作也不会阻塞,而是放到事件队列中等待当前操作执行完再继续执行。Flutter在当执行到await
时,保存当前的上下文,并将当前位置标记为待处理任务,用一个指针指向当前位置,并将待处理任务放入当前线程的队列中。在每个事件循环时都去询问这个任务,如果需要进行处理,就恢复上下文进行任务处理。
多线程:
而async和await是沿用自js的,但有一点要注意的是,js是脚本语言,所以必须是单线程的,到这里就足够了。而flutter是手机框架,很可能我们要进行很耗时的IO操作,这仅仅凭异步是解决不了的,必须要多线程。这里flutter引入了新的方案,叫做isolate。
但isolate和普通的Thread还不同,它具有独立的内存,isolate间的通信通过port来实现,这个port消息传递的过程是异步的。实例化一个isolate的过程也就是实例化isolate这个结构体、在堆中分配线程内存,配置port。
感觉从操作来看其实isolate更像是进程,而实际async则更像是线程操作。
loadData() async {
// 通过spawn新建一个isolate,并绑定静态方法
ReceivePort receivePort =ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// 获取新isolate的监听port
SendPort sendPort = await receivePort.first;
// 调用sendReceive自定义方法
List dataList = await sendReceive(sendPort, 'https://jsonplaceholder.typicode.com/posts');
print('dataList $dataList');
}
// isolate的绑定方法
static dataLoader(SendPort sendPort) async{
// 创建监听port,并将sendPort传给外界用来调用
ReceivePort receivePort =ReceivePort();
sendPort.send(receivePort.sendPort);
// 监听外界调用
await for (var msg in receivePort) {
String requestURL =msg[0];
SendPort callbackPort =msg[1];
Client client = Client();
Response response = await client.get(requestURL);
List dataList = json.decode(response.body);
// 回调返回值给调用者
callbackPort.send(dataList);
}
}
// 创建自己的监听port,并且向新isolate发送消息
Future sendReceive(SendPort sendPort, String url) {
ReceivePort receivePort =ReceivePort();
sendPort.send([url, receivePort.sendPort]);
// 接收到返回值,返回给调用者
return receivePort.first;
}
(代码引用自https://lequ7.com/2019/04/26/richang/shen-ru-li-jie-Flutter-duo-xian-cheng/)
这点不做详述,理解了Dart的多线程以及网络请求的基本原理也很容易搞懂这块(https://flutterchina.club/networking/)。
文章不从性能进行分析,而是仅仅从语法方面进行入门比对。其实如果做过Web前端的童鞋学期Flutter来说应该会比较简单,因为Flutter中的Widget布局方式完全类似于html和css。而Dart语言本身有非常像js,尤其是ES6特性引入很多语法糖后的js,这些语法糖大大简化了代码的复杂度。