Flutter探索:原生视角下的全面回顾

发表时间: 2021-04-14 16:00

简介

Flutter是一款移动应用程序SDK,一份代码Dart代码,同时生成iOS和Android两个应用程序,通过自行绘制来做渲染,达到高性能、两端样式高度一致,带来的缺点就是丧失了平台特性。


整体架构

通过FlutterEngine抽象具体实现,Framework做到与平台无关。


一切皆为widget

Widget是Flutter应用程序用户界面的基本构建块。iOS、Android 将视图、控制器、布局和其他属性分离,Flutter是统一对象模型:widget。(跟AsyncDisplayKit思路类似)


渲染流程

开发者通过Widget构建出Widget树,然后engine会把根据Widget树构建出Element树,然后形成渲染树,进而展示在屏幕,用户看到一个页面。

Flutter的UI系统包含三棵树:Widget树、Element树、Render树。他们的依赖关系是:Element树根据Widget树生成,而Render树又依赖于Element树。可以看到Element是框架内部连接widget和RenderObject的纽带,大多数时候开发者只需要关注widget层即可,但是widget层有时候并不能完全屏蔽Element细节,所以Framework在Widget中通过build方法参数又将Element对象也传递给了开发者,这样一来,开发者便可以在需要时直接操作Element对象。

如果没有widget层,单靠Element层是否可以搭建起一个可用的UI框架?

是可以的。Widget等同于MVVM中的model,去掉Widget 就跟简单的纯原生开发基本一致了。

Element树的存在,以及Widget树 与 Element树 的关系, 很好地解决了React的diff性能问题。


基础对象widget

StatelessWidget:

不持有数据,函数式的控件。


StatefulWidget:

State是它的数据部分,分离业务数据和样式数据。state是在element树中的,state中的数据算是控件自身的数据,同类控件复用的时候有区别。

setState方法只是刷新当前的StatefulWidget,通过划分对象,可以做到局部刷新。

如果runtimeType和key一致的话(没有key就只对比runtimeType),那么就认为该element仍然是有效的,可用复用,控件自身的属性依然会在。
解决办法1:给widget指定key
解决办法2:覆写State的didUpdateWidget方法


widget的生命周期

initState:当Widget第一次插入到Widget树时会被调用,对于每一个State对象只会调用一次。
didChangeDependencies:当State对象的依赖发生变化时会被调用,InheritedWidget。
build:用于构建Widget子树的,didChangeDepenencies执行后,这个方法也会被调用。
didUpdateWidget:在widget重新构建时,调用Widget.canUpdate来检测同一位置的新旧节点,返回true则会调用此回调。
deactivate:当State对象从树中被移除时,会调用此回调。
dispose:当State对象从树中被永久移除时调用;通常在此回调中释放资源。


数据传递

InheritedWidget:widget组数据传递

static ShareDataWidget of(BuildContext context) {
return
context.inheritFromWidgetOfExactType(ShareDataWidget);

//上面的函数会注册依赖关系,下面的函数不会注册依赖关系
//return
context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;

}

widget的build函数中使用了InheritedWidget的of,就会被注册依赖关系,触发didChangeDependencies();不想触发就修改of内部实现。

其实也可以把操作代理回父widget中进行处理,这个组件只是相当于把父widget中的逻辑处理给分离出去进行精细化处理。


EventBus:widget组数据传递

import 'package:event_bus/event_bus.dart';

EventBus bus = new EventBus();

bus.on("login", (arg) {});//initState中添加监听

bus.off("login");//dispose 移除监听

bus.emit("login", userInfo);//触发登录事件

可以做到跨页面传递数据。dispose 移除监听,不然会内存泄漏。


app生命周期

自定义 widget 使用 with WidgetsBindingObserver ,initState中 WidgetsBinding.instance.addObserver(this),实现监听 flutterApp的生命周期。

当app生命周期变化的时候,会调用widget的didChangeAppLifecycleState 方法。


flutter消息循环机制

在Java和OC中,如果程序发生异常且没有被捕获,那么程序将会终止,但是这在Dart或JavaScript中则不会!Java和OC都是多线程模型的编程语言,任意一个线程触发异常且该异常未被捕获时,就会导致整个进程退出。但Dart和JavaScript不会,它们都是单线程模型。

Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。微任务队列的执行优先级高于事件队列。

当一个返回Future的函数被调用的时候,把自己放入队列和返回一个未完成的Future对象,之后当值可用时,Future带着值变成完成状态。如果Future在then()被调用之前已经完成计算,那么任务会被添加到微任务队列中,并且该任务会执行then()中注册的回调函数。也可以通过Future.microtask(…)方法向微任务队列插入一个任务。执行太多,表现为卡顿。

//顺序执行
getName2().then((_) => getName1()).then((_) => getName3());
//使用wait,一并返回完成值
Future.wait([getName2(), getName1(), getName3()]).then((List re) {
re.forEach((i) => print(i));
});


flutter与native通信

1、BasicMessageChannel,用于传递字符串和半结构化的信息,是全双工的,可以双向请求数据。
2、MethodChannel,用于传递方案调用,即 Dart 侧 可以调用原生侧的方法并通过 Result 接口回调结果数据。
3、EventChannel:用户数据流的通信,即 Dart 侧监听原生侧的实时消息,一旦原生侧产生了数据,立即回调给 Dart 侧。
4、methodCall 需要编解码,其实主要的消耗都在编解码上了,因此,MethodChannel 并不适合传递大规模的数据。比如我们想调用摄像头来拍照或录视频,但在拍照和录视频的过程中我们需要将预览画面显示到我们的 Flutter UI中。为此,Flutter 提供了一种基于 Texture 的图片数据共享机制。Texture 和 PlatformView


页面跳转

一个engine可以对应多个FlutterVC,所以push新页面,可以是原生形式的push一个FlutterVC,也可以是FlutterVC内的Widget之间的切换。

每新开一个FlutterVC内存上升比较大,但是FlutterVC内push又会提升和native混编的复杂性。


crash+日志

通过设置FlutterError.onError的方法内容,可以收口所有的crash

通过设置runZoned的全局函数,可以收口所有日志。

https://book.flutterchina.club/chapter2/thread_model_and_error_report.html


参考:

https://book.flutterchina.club/

https://github.com/Solido/awesome-flutter

https://flutterawesome.com/

https://zhuanlan.zhihu.com/p/160189801

https://www.yuque.com/xytech/flutter/