Flutter的线程模型

闲鱼的flutter分享:https://zhuanlan.zhihu.com/p/38026271
官方提供的文档:https://github.com/flutter/flutter/wiki/The-Engine-architecture#threading
flutter和native应用开发不同,flutter的主线程或者说UI线程指的并不是native开发中认知的主线程,因为flutter自行实现了UI渲染和交互,所有逻辑运行在单独的一个线程中,这个单独的线程被称作flutter UI Task Runner Thread。因此,native主线程的阻塞不会导致flutter界面的卡顿,但native的事件依然会因为native主线程的卡顿导致无法正常派发手势和vsync,从而flutter无法及时接收事件并处理。

Flutter用户界面

fluuter pipeline工作原理:https://www.youtube.com/watch?v=UUfXWzp0-DU
flutter renderSilver介绍:https://www.youtube.com/watch?v=Mz3kHQxBjGg

Flutter中的界面是使用Layer构建的,因此有必要了解一下layer的触摸事件响应、布局方式、绘制流程。
不过,在此之前需要先了解下Flutter的Widget/Element机制。

Widget和Element

Flutter中,我们编写界面是通过Widget编写的,每个Widget都对应着一个Element,Element持有一个创建它的Widget的引用,界面绘制时是根据Element来绘制的。
当界面有变化需要重建Element树时,一颗新的Widget树会被创建,但Element会尽可能的被保留,flutter在根据Widget树创建Element树时,会尝试检查上次创建的Element,如果Element的runtimeType和key都和之前的一样,就只是会更新Element所绑定的Widget对象,而不会重新创建Element。这个行为常常会导致同一层级下多个相同类型的StatefulWidget在更新时由于被错误的复用,而产生不符合预期的行为,因为StatefulWidget的状态并不保存在Widget里,而是保存在Element中。

  • 当复用发生时,Element的Widget引用会被更新成这一帧的新的Widget,并调用Widget#build获取新的Widget树,用新的Widget树来更新Element的状态,这个过程对于StatelessWidget是OK的,毕竟StatelessWidget的所有状态都是通过Widget体现的;
  • 对于StatefulWidget,上面的过程就不对了,同类型的StatefulWidget发生复用时,虽然Widget引用被更新成了当前帧的Widget,但是State并没有改变,也就是说,如果本来某一个level上有4个StatefulWidget,某个时刻第1个Widget被干掉了,如果不提供key,那么下面的三个Widget由于runtimeType相同,element就会发生复用,导致原本被干掉的第一个Element的Widget被更新为现在的第一个Widget,但是State还是原本被干掉的那个Widget创建的State对象,于是创建出来的新的Widget树混合了新的Widget和老的State的状态,BOOOOOM!
  • 看不明白?这里还有一篇非常详细的说明:https://medium.com/flutter/keys-what-are-they-good-for-13cb51742e7d

深入理解Flutter渲染

Flutter中,Widget是Element的配置对象,其唯一的作用就是用来创建出Element。在Flutter framework中,只会维护Element的状态,不会维护Widget的状态。每当新的一帧需要显示时,Flutter会根据当前Element tree来构建出一个RenderObject tree,然后再将RenderObject tree交给engine,由engine根据RenderObject tree来构造出Layer tree,最后根据Layer tree拿到渲染所需的GPU渲染数据和命令,通过GPU完成一帧的渲染,然后将这一帧交给平台的窗口系统,我们就能够看到flutter渲染出来的界面了。
每个Flutter App都通过runApp来显示出初始的Widget。在这个过程背后,和UI渲染有关的对象主要是:

  • RendererBinding,这个对象初始化时注册了Frame监听器来响应刷帧事件驱动Widget和Element构建,最终生成RenderObject树,完成flutter framework的工作,将接力棒交给flutter engine;
    • RenderObject,由上面的RendererBinding创建,继承自RenderObject的RenderView作为根节点;
    • PipelineOwner,由上面的RendererBinding创建,提供了一系列方法用来维护RenderObject树状态;
    • RenderView,Flutter App的root render object,它和PipelineOwner之间相互引用;
  • SchedulerBinding,window的onBeginFrame和onDrawFrame都是通过它定义的方法作为入口点触发的;
  • BuildOwner,Widget管理器,提供了一系列用于维护Element tree状态的方法,Element状态更新就是通过调用它提供的方法scheduleBuildFor来触发的;
  • RenderObjectToWidgetAdapter,App的根Widget,给RenderObjectToWidgetElement作为配置使用,并且在createRenderObject方法中返回RenderView;
  • RenderObjectToWidgetElement,App的根Element,当App业务Element创建好RenderObject,调用它的insertChildRendedrObject时,它会将业务RenderObject转而交给RenderView;
  • PaitingContext,全局的Layer管理对象,提供了static的repaintCompositedChild和pushLayer方法;
  • RenderBox,是一个接收Constraints的RenderObject;

下面的图是三颗重要的树的根对象:RenderView是RenderObject的树根,RenderObjectToWidgetAdapter是Widget树根,RenderObjectToWidgetElement是Element树根,后续分析代码时一定要明确当前究竟处理的是哪棵树,目的是构造哪颗树。
其实,还有一颗树这里没提及到,就是Layer树,不过Layer树是等到RenderObject树构造好了之后,才开始构建的,相对比较独立,更多的应该算到Engine层面,而不是Framework层面。
flutter render.png

Flutter的启动

native部分简略描述,不同的端有不同的过程,不过大体上是在主线程创建一个能用于GL绘制的View,然后将这个View的句柄传给Flutter Engine,Engine会创建好一个PlatformView。然后,Engine还会将主线程作为Platform Thread,并再从native创建三个Thread:UI、GPU、IO,每个Thread内都运行着一个和平台对应的消息队列实现,其中UI Thread就是dart代码执行的线程,UI Thread正对应着dart中的root Isolate,另外两个线程则是只在Engine中使用的,GPU thread看名字是用来做GPU渲染的,IO Thread没看到有什么用,dart本身提供了io库,并且io库的方法都是用dart vm内的线程池处理的,这里的IO看着不像是做io操作的,而且就算是做io操作,一个线程也太少了。
当native完成了plugin信息收集,JNI方法绑定,各种基本初始化后,就会启动Dart root isolate,isolate中执行的第一个方法就是runApp。这里面:

  • 调用了WidgetFlutterBinding#ensureInitialized,创建了WidgetFlutterBinding,同时触发了一系列Binding初始化,下面我们主要关注的是RendererBinding
    • RendererBinding初始化时创建了RenderView,调用prepareInitialFrame,在真正触发首帧渲染前做准备工作,主要是注册各种事件监听;
      • 骚操作:注意Setter逻辑!!!
        • flutter框架源码中,没有以下划线开头的属性一般都代表setter或者getter,看到对这种属性的赋值和取值一定要小心,说不定它背后的setter或者getter就有一坨代码逻辑!!如果不小心忽略了,恐怕得浪费几个小时来到处分析代码。
        • 上面提到的代码中就有这样的坑:renderView赋值触发了setter,在这个setter里会将设置进来的renderView赋值给pipelineOwner.rootNode,进而又触发了rootNode的setter,调用了pipelineOwner.attach(renderView)为renderView和pipelineOwner建立了关联。
  • 调用了WidgetsFlutterBinding#scheduleAttachRootWidget,构造了根Widget和根RenderObject,并将它们和BuildOwner关联,然后锁定setState,调用根Element的mount方法,这个方法里会调用根Element的_rebuild,进而调用到Element#updateChild,这是flutter widget机制的核心;
    • 如果element之前没有widget,就直接使用inflateWidget,利用我们传给runApp的widget渲染出来一个Element,通常会是一个StatefulElement,然后调用element的mount方法。
    • 由于我们的Element是ComponentElement的子类,在重写的mount方法里,除了走Element的mount来更新一些Element属性以外,还会走_firstBuild()方法,调用rebuild(),perfromRebuild();
      • 在ComponentElement的performBuild的实现里,调用了build(),来根据我们自己重写的build方法拿到子Widget,然后继续调用了updateChild方法,就是上面提到的根Element的_rebuild会调用的那个,递归的来建立出来整颗Element树,当然不同的Element具体逻辑会有差别,这里只是分析了ComponentElement的;
    • 如果Element是RenderObjectElement,重写的mount方法里只会创建renderObject,并通过attachRenderObject来把RenderObject对象挂到父RenderObject上,由于根Element就是RenderObjectElement的子类,所以肯定是能挂上的;另外performRebuild方法里也只会更新自己的renderObject对象;
      • RenderObjectElement的一些子类是会有child的,它们在mount时还会
    • 结合上面的描述,在第一次调用完mount方法后,ComponentElement走了build逻辑建立好了Element树,RenderObjectElement也创建好了自己的RenderObject并通过层层挂载建立好了一颗RenderObject树;
  • 接着调用WidgetsFlutterBinding#scheduleWarmUpFrame(),用来尽快准备渲染好第一帧,在这个方法里,用到了Timer.run(lambda)的写法,其作用是将lambda移动到当前event的末尾执行,dart称之为“microtask”,在microtask中先后调用了handleBeginFrame、handleDrawFrame、
    • 主要的渲染工作是由RendererBinding中的逻辑完成的,在RendererBinding初始化时,向Binding注册了一个FrameCallback,这个callback会在handleDrawFrame调用中被触发,这个callback是一个RendererBinding中定义的方法:_handlePersistentFrameCallback,里面调用了drawFrame,先后调用了pipelineOwner的flushLayout、flushCompositingBits、flushPaint,以及renderView的compositeFrame方法,最终将渲染需要的GPU指令创建好并发送到engine,由engine负责利用GPU完成这一帧的渲染。
      • PipelineOwner#flushLayout - 让render object更新它们的layout,计算位置和大小,如果render object没有改变不需要修改任何渲染指令,就不需要把自己标记为dirty,否则会标记dirty;
        • 在这一步,实际调用的是RenderObject#_layoutWithoutResize,这个方法又调用了performLayout,由于我们的根RenderObject对象实际是RenderView,因此实际调用的是RenderView#performLayout,这个方法中取得了当前窗口大小,构造了一个min和max约束都定死的BoxConstraints,调用了子RenderObject的layout方法;
        • 在子RenderObject的layout方法中,又会判断是否尺寸仅由父object提供的constraints决定,如果是的话,就通过performResize方法更新自己的尺寸,如果不是,也就是尺寸还要考虑自己的子object,那么尺寸就要通过performLayout方法决定,在这个方法里必须完成所有子view的尺寸的测量;
        • 最后,会通过markNeedsPaint将自己标记为需要重绘,默认情况下也会触发父RenderObject的markNeedsPaint,不过可以通过一个标记:repaintBoundary,来让flutter为当前RenderObject创建一个OffsetLayer,这样就可以和父分别绘制到不同的layer上,最后再合成了;
      • PipelineOwner#flushCompositingBits - 更新dirty状态render object的compositing bits,这个过程是个递归过程;
      • PipelineOwner#flushPaint - 遍历render object,为每个标记为composited的render object都建立单独的layer(OffsetLayer),调用这些render object的paint方法,将绘制命令通过一个PictureLayer记录下来,这个PictureLayer会被设置为上面创建的OffsetLayer的child,后续会利用这个对象来做Scene的构建;
        • 这一步会遍历_nodesNeedingPaint中的RenderObject对象,利用PaintingContext的repaintCompositedChild方法来为它们创建OffsetLayer,利用OffsetLayer为它们创建PaintingContext实例,最后调用RenderObject的paint方法,让它们把绘制操作记录到context上,也就是layer上,绘制完毕后,将结果记录在renderObject.layer.picture上;
        • 注意只有isRepaintBoundary = true的RenderObject才会在被标记为dirty的同时被PipelineOwner加入到_nodesNeedingPaint列表中;
      • RenderView#compositeFrame - 将上一步建立好的layer tree提交给引擎,显示界面
        • 这一步创建了一个SceneBuilder,并将以RenderView.layer为根的整颗layer tree添加到SceneBuilder,我们通过Canvas记录的绘制命令最后是通过PictureLayer#addToScene方法,调用到Engine的SceneBuilder_addPicture native方法来传递给引擎的,如果某个Layer没有任何变化,那就会通过SceneBuilder_addRetained native方法来告知引擎继续使用之前的信息;
        • 在将所有信息通过Builder记录完毕后,调用了build()方法创建出来Scene,然后使用window.render方法,最终让Engine开始GPU渲染,将界面渲染出来,不过这些过程统统都调用的是Engine native方法了;
  • 总算完了!

由setState触发的UI刷新

文字描述太啰嗦了,还是弄个时序图。

flutter的页面启动和更新时序

image.png

Flutter手势处理

https://flutter.dev/docs/development/ui/advanced/gestures
根据Fluuter的文档,Flutter的手势处理和android的有较大的区别,相比android,flutter在确定某个手势应该由哪个widget处理时,判断逻辑简化了很多。flutter建议开发者使用GestureDetector,如果一个手势区域被多个GestureDetector监听,flutter总是会尽可能早的识别事件,并将事件通知给GestureDetector的回调。android中的神奇操作不再被支持,这问题不大,毕竟手势冲突本来就是些比较反人类的产品设计需求,比如在上下滑动的scrollView中嵌套的子View支持上下滑动的手势能够控制是优先让子View处理还是ScrollView处理什么的,就算开发出来了,也只会引起用户的困惑。

Flutter的页面管理

flutter中,使用Navigator类管理页面跳转。提供了push和pop方法,用于跳转到特定的Widget实例,以及返回。所谓页面跳转,就是将当前Widget暂存,用新的Widget替换当前Widget称为页面Widget,之后返回再取回暂存的Widget并显示。

Flutter的资源管理

Flutter App除了包含代码,还需要包含资源。资源文件会被打包并可以在运行时使用。常用的资源文件包括JSON数据文件、配置文件、图标和图片等。
要使用资源,需要在pubspec.yml中使用assets来指定资源,可以挨个将资源文件列举出来,也可以直接指定目录。通常资源文件应该存放在/assets/目录下,并且pubspec.yml中直接将该目录指定为资源目录。
pubspec.yml中的assets指定了所有需要被打包的资源,指定资源时的路径是相对于pubspec.yml所处位置的相对路径,资源具体放在哪个目录下没有强制要求。打包过程中,所有资源会被打包成asset bundle,供App在运行时使用。

资源类目

如果pubspec.yml中通过assets指定了一个资源目录(或者直接指定文件名),这个资源目录下就可以通过建立子文件夹来实现资源类目的概念,通过将不同风格的bg.png文件分别存放在资源目录下,以及名叫dark的子目录下,就创建了默认资源类目和dark资源类目,打包时都会被打包到asset bundle。

代码中使用资源

代码中通过AssetBundle这个类来访问资源。我们平时主要会用到loadString()来加载字符串,以及使用load()来加载二进制文件内容。

字符串的加载

每个flutter app都有一个rootBundle对象用于访问默认asset bundle,可以通过引入package:flutter/services.dart中的rootBundle全局静态变量来访问该对象。
不过,通常不建议这么做,应该尽可能使用当前BuildContext来访问资源,因为这样flutter就有机会对资源加载做一些额外逻辑,比如支持本地化,或者支持测试。使用DefaultAssetBundle.of()方法传入BuildContext来间接访问资源。

图像的加载

flutter可以根据当前的像素密度比来加载合适分辨率的图像。fluttrer中Image widge构造参数image接收一个AssetImage类型的参数,使用AssetImage的默认构造函数参数来指定图像路径即可。

根据像素密度声明图像

AssetImage能够将图片加载映射到和当前像素密度最匹配的图像。为此需要将图像资源按照特定的目录格式来存放。
在asset目录中,需要创建2.0x、3.0x这样的目录,里面存放不同像素密度对应的图像文件,没有被存放在这样的目录中的图像文件被认为是1.0x的。如果设备的像素密度比不是恰好是整数,那么会四舍五入来决定具体加载的图像,并且AssetImage在确定自身尺寸时,也会根据图像的像素密度比进行调整,比如如果是从2x加载的4080的图像,那么AssetImage会认为自己最合理的大小应该是2040。

从依赖包加载资源,以及打依赖包时打包资源

fluuter文档没说清楚。
read the funking source code。

Flutter状态管理

Flutter App使用声名式的风格来管理App状态。Widget显示状态由状态唯一确定,传统的native开发中Widget显示状态可能是由很多条代码执行链路来改变的,导致维护困难,出现问题时难以排查。

App状态和Ephemeral/[ɪˈfemərəl]/状态的区别

当我们在讨论状态管理时,并不是在讨论flutter app的整个内存状态,而是为了重建App Widget所需的这部分状态,是App自己需要管理维护的状态。有很多状态都是由Flutter Engine或者Flutter framework维护的,它们不影响App Widget状态的确定逻辑,并不在我们的讨论范畴之内。
App需要维护两种状态,一种是只有当前Widget关心的状态,比如scrollView滑动到哪里了,比如PageView当前在哪一页,又比如当前动画进行到哪一步了,通常这些状态并不会被其他Widget使用,只要存放在Widget自己的State中就可以,这类状态是Ephemeral状态,包含这类状态的Widget被实现为StatefulWidget。
另一种状态是需要在App的多个Widget间传递的,通常顶层的Widget会作为StatefulWidget持有这些状态,下层的Widget则是StatelessWidget,自身不持有状态而是根据顶层Widget来构建自身。

没有明确的设计决策指南

哪些状态应该被封装在特定Widget内部,哪些状态应该被上层widget持有?没有明确的答案,最极端的情况下只有顶层Widget有状态,所有下层Widget都由顶层Widget指导构建,任何改变都通过调用顶层Widget State的setState来进行。
只要谨遵KISS原则,在简单性和可读性之间找到平衡,就是好的设计。

简单App状态管理:Provider机制

有多种管理App状态的机制,比如react开发者熟悉的redux,以及由flutter提供的InheritedWidget,但这里我们介绍一种更加简单的,由flutter直接支持的机制:Provider。
Provider涉及到三个概念:

  • ChangeNotifier,一个Data Model,定义了状态,以及修改状态的方法,当状态被修改时,必须调用notifyListeners方法来触发Consumer的重新构建;
  • ChangeNotifierProvider,一个上层Widget,定义了关联的ChangeNotifier,其下层包含Consumer
  • Consumer,一个下层Widget,定义了如何根据ChangeProvider构建Widget,其builder方法有三个参数,分别是:context、changeNotifier、child,其中context就是build方法一般都会有的BuildContext,第二个是要被consume的ChangeNotifier对象,我们使用这个对象的状态来构建Widget,第三个参数是Consumer在构造时传入的child,用来提供优化的可能性,如果你发现本次Model变化导致的重新构建的Widget中的某个部分总是不需要任何变化,就可以利用child做复用了。(不过这件事我们自己也能做,也没什么复杂的,如果不止一个widget需要复用,这东西就没用了,还是要自己搞,框架做这种事情是不是有点多余?)

有时候我们的某个下层widget仅仅是想触发ChangeNotifier的某些方法,来触发其他Consumer的重新构建,但这个widget自己却不依赖ChangeNotifier的状态因此不需要重新构建,此时就可以通过调用Provider.of(context, listen: false)来获取ChangeNotifier实例,调用其状态改变方法。

其他状态管理方案

https://flutter.dev/docs/development/data-and-backend/state-mgmt/options
资料最多的还是flutter的scoped model以及redux。

Flutter嵌入native view

在Flutter中有看到疑似的类,但官方文档并没有任何说明,可能是在开发中?之后再看看吧。不过,Flutter嵌入Native听上去就是个伪命题,毕竟一般都是Native嵌入Flutter。遇到具体场景再研究。

Native嵌入Flutter

flutter 1.12支持将一个全屏的Flutter实例嵌入到App中,仅此而已,如果你想要做以下事情,需要你自己进行测试,因为Flutter团队不保证这样做的后果:

  • 同时运行多个Fluuter实例,或者显示非全屏的Flutter view;
  • 让Flutter在运行在无界面的后台模式,这项工作还没完成;
  • 将Flutter library打包到独立的so,或者将多个Flutter library打包到一个App中;
  • Plugin应该按照新的Plugin API来实现,否则可能会有不符合预期的行为(新的Plugin API考虑了Flutter是被嵌入到Native App的可能性,并对各种场景做了处理,老的API没有对某些场景做合适的处理,遇到某些场景就挂了);

因为目前flutter支持的接入方式非常有限,只支持全屏单实例这种模式,因此具体接入步骤看起来还比较简单,直接参考https://flutter.dev/docs/development/add-to-app

Flutter动画

Flutter中,AnimationController是动画框架的核心实现类,是动画框架的驱动,负责根据传入的参数来在每一帧绘制时生成一个0到1之间的线性插值浮点数值,并且提供了一些控制动画的API。
Flutter中的核心动画API则是Animation接口,这个接口提供了动画监听方法,动画过程中每一帧都会通知Listener,让Listener能够触发Widget重绘。
另一个辅助接口则是Animatable,这个接口允许实现者自己实现插值方案,Tween就是通过实现这个方法来将一个0至1之间的线性插值属性重新映射为不同范围、不同插值曲线的值的,但就如上一段所说,这只是个辅助方法,必须配合AnimationController使用,真正负责管理动画启停的、触发每一帧回调的还是AnimationController,所以Animatable构造好之后,还是得通过animate方法把自己注册到AnimationController,然后获得一个Animation对象,再进一步注册Listener,最后还是需要通过AnimationController的方法来开启动画。

例子就不贴了,flutter的中文网站翻译很不错,直接参考即可:https://flutterchina.club/tutorials/animation/#%E5%8A%A8%E7%94%BB%E7%A4%BA%E4%BE%8B

Navigator与Dialog

https://api.flutter.dev/flutter/widgets/Navigator-class.html
在Flutter中,Navigator不仅可以在页面之间路由,同时也用来显示Overlay,Overlay是背景透明的界面,通过为Overlay widget指定半透明的背景和一个对话框,就可以实现原生的对话框样式。
在Flutter中我们不需要自己编写一个Widget来管理Navigator,就算你不需要Android的Material样式也不需要iOS的Cupertino样式,也可以使用WidgetsApp。国内一般倾向于使用iOS样式来开发Android,而不是反过来,所以更多的会使用Cupertino样式。