正好趁着这个假期,把前段时间掌握的Flutter底层知识点记录下来,日后还会持续更新哈!
Flutter与React Native的本质区别:
React Native,通过JavaScript虚拟机扩展,间接的通过一些桥接方式,调用原生控件,由Android和iOS系统进行控件的渲染;本身是不做任何渲染的, 由于各个系统的控件是不一样的,会导致UI不一样! 性能较差!
Flutter则是通过Skia引擎,将视图数据传递到GPU,然后渲染到屏幕上的,不需要在依赖于原生控件! 同时保证了Android/iOS平台UI都保持一致! 每个平台都会有对Skia支持!
Flutter绘制原理- UI 线程用Dart来构建图层树
- 图层树在GPU 线程进行合成
- 合成后的视图数据提供给Skia 引擎
- Skia 引擎通过OpenGL将显示内容提供给GPU渲染
Skia已是Android官方的图像渲染引擎了,因此Flutter Android SDK无需内嵌Skia引擎就可以获得天然的Skia支持;而对于iOS平台来说,由于Skia是跨平台的,因此它作为Flutter iOS渲染引擎被嵌入到Flutter的 iOS SDK中,替代了iOS闭源的Core Graphics/Core Animation/Core Text,这也正是Flutter iOS SDK打包的App包体积比Android要大一些的原因。
Flutter 热重载
JIT(Just In Time),指的是运行时编译,在Debug模式中使用,可以动态下发和执行代码,启动速度快,但执行性能受运行时编译影响。
JIT编译模式示意图
AOT(Ahead Of Time),指的是静态编译,在Release模式中使用,可以为特定的平台生成稳定的二进制代码,执行性能好、运行速度快,但每次执行均需提前编译,开发调试效率低。
AOT编译模式示意图
热重载之所以只能在Debug模式下使用,是因为Debug模式下,Flutter采用的是JIT动态编译(而Release模式下采用的是AOT静态编译)
由于JIT属于动态编译,JIT编译器将Dart代码编译成可以运行在Dart VM上的Dart Kernel,而Dart Kernel是可以动态更新的,这就实现了代码的实时更新功能!
热重载大体流程
热重载的流程可以分为: 工程改动、编译Kernel产物、推送更新、代码合并、Widget重建5个步骤:
- 工程改动。热重载模块会逐一扫描工程中的文件,检查是否有新增、删除或者改动,直到找到在上次编译之后,发生变化的Dart代码。
- 编译产物。热重载模块会将发生变化的Dart代码,通过编译转化为增量的Dart Kernel文件。
- 推送更新。热重载模块将增量的Dart Kernel文件通过HTTP端口,发送给正在移动设备上运行的Dart VM。
- 代码合并。Dart VM会将收到的增量Dart Kernel文件,与原有的Dart Kernel文件进行合并,然后重新加载新的Dart Kernel文件。
- Widget重建。在确认Dart VM资源加载成功后,Flutter会将其UI线程重置,通知Flutter Framework重建Widget。
Flutter提供的热重载在收到代码变更后,并不会让App重新启动执行,而只会触发Widget树的重新绘制,因此可以保持改动前的状态,这就大大节省了调试复杂交互界面的时间。
Flutter是单线程的,那么是如何来处理网络通信、IO操作它们返回的结果呢?
是通过事件循环(Event Loop)!
这里有一个前提,那就是我们的App绝大多数时间都在等待。比如,等用户点击、等网络请求返回、等文件IO结果,等等。而这些等待行为并不是阻塞的。比如说,网络请求,Socket本身提供了select模型可以异步查询;而文件IO,操作系统也提供了基于事件的回调机制。
基于这些特点,单线程模型可以在等待的过程中做别的事情,等真正需要响应结果了,再去做对应的处理。
在Dart中,实际上有两个队列,一个事件队列(Event Queue),另一个则是微任务队列(Microtask Queue)。在每一次事件循环中,Dart总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程!
队列
我们先看微任务队列,表示一个短时间内就会完成的异步任务。从上面的流程图可以看到,微任务队列在事件循环中的优先级是最高的,只要队列中还有任务,就可以一直霸占着事件循环。
我们通常很少会直接用到微任务队列,就连Flutter内部,也只有7处用到了而已(比如,手势识别、文本输入、滚动视图、保存页面效果等需要高优执行任务的场景)
微任务创建方式:
scheduleMicrotask(() => print('This is a microtask'));
异步任务我们用的最多的还是优先级更低的Event Queue。比如,I/O、绘制、定时器这些异步事件,都是通过事件队列驱动主线程执行的。
Dart为Event Queue的任务提供了一层封装,叫作Future。把一个函数体放入Future,就完成了从同步任务到异步任务的包装。
在我们声明一个Future时,Dart会将异步任务的函数执行体放入事件队列,然后立即返回,后续的代码继续同步执行。而当同步执行的代码执行完毕后,事件队列会按照加入事件队列的顺序,依次取出事件,最后同步执行Future的函数体及后续的then。
then与Future函数体共用一个事件循环。而如果Future有多个then,它们也会按照链式调用的先后顺序同步执行,同样也会共用一个事件循环! 但如果Future函数体内是 null,那么then事件会被添加到微队列任务中!
为了方便理解,从如下代码进行分析:
Future(() => print('f1'))
.then((_) async => await Future(() => print('f2')))
.then((_) => print('f3'));
Future(() => print('f4'));
- 按照任务的声明顺序,f1和f4被先后加入事件队列。
- f1被取出并打印;然后到了then。then的执行体是个future f2,于是放入Event Queue。然后把await也放到Event Queue里。
- 这个时候要注意了,Event Queue里面还有一个f4,我们的await并不能阻塞f4的执行。因此,Event Loop先取出f4,打印f4;然后才能取出并打印f2,最后把等待的await取出,开始执行后面的f3。
我们在使用await进行等待的时候,需要在等待调用上下文函数中加上了async关键字,为什么要加这个关键字呢?
答: Dart中的await并不是阻塞等待,而是异步等待。Dart会将调用体的函数也视作异步函数,将等待语句的上下文放入Event Queue中,一旦有了结果,Event Loop就会把它从Event Queue中取出,等待代码继续执行。
Isolate
Dart是基于单线程模型的,但为了进一步利用多核CPU,Dart也提供了多线程机制,即Isolate。
在Isolate中,资源隔离做得非常好,每个Isolate都有自己的Event Loop与Queue,Isolate之间不共享任何资源,通过发送管道(SendPort)实现消息通信机制,因此也就没有资源抢占问题。
关于Isolate的使用,代码就不贴出来了,请各位看官自行百度哈~
Flutter渲染三棵树
Widget是什么?
Widget就是一个个描述文件,描述这个View(界面)应该长什么样子,这些描述文件在进行状态改变时会不断的build。在重新build它的描述时,框架会和之前的描述进行对比,是否需要刷新!
Element是什么?
Element是一个Widget的实例,在Element树中详细的位置。
那么Element树存在的意义是什么?
减少渲染带来的性能损耗,提高渲染效率 !
因为Widget具有不可变性,但Element却是可变的。实际上,Element树这一层将Widget树的变化做了抽象,只将真正需要修改的部分同步到真实的RenderObject树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。
Element什么时候创建?
每一次创建Widget的时候,会创建一个对应的Element,Element保存着对Widget的引用,然后将该Element插入树中。在调用mount方法时,会同时使用Widget来创建RenderObject,并且保持对RenderObject的引用!
注意: 并不是所有的Widget都会去创建RenderObject,Widget继承RenderObject相关子类才会去创建,比如Text没有继承,则不会去创建RenderObject。
三者关系如下:
RenderObject是什么?
渲染树上的一个对象! 主要负责实现视图布局、绘制的对象,其中有 markNeedsLayout、performLayout、markNeedsPaint、paint等方法。
而渲染对象树在Flutter的展示过程分为四个阶段,即布局、绘制、合成和渲染。 其中,布局和绘制在RenderObject中完成,Flutter采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把它们绘制到不同的图层上。绘制完毕后,合成和渲染的工作则交给Skia搞定。
build的context是什么?
本质上BuildContext就是当前的Element
垃圾回收机制
当Flutter引擎被侦测到App处于闲置的状态,并且没有用户交互的时候,进行GC(垃圾回收)操作
垃圾回收机制
新生代:
新对象被分配到连续、可用的内存空间,这个区域包含两个部分:活跃区和非活跃区,新对象在创建时被分配到活跃区、一旦填充完毕,仍然活跃的对象会被移动到非活跃区,直到所有存活的Object都被移动到非活跃区,不再活跃的对象会被清理掉,然后非活跃区变成活跃区,活跃区变成非活跃区,以此循环。
注意:新生代阶段主要是清理一些寿命很短的对象,比如StatelessWidget。当它处于阻塞时,它的清理速度远快于老生代的mark-sweep方式。所以性能影响非常低。
老生代:
在新生代阶段未被回收的对象,将会由老生代收集器管理新的内存空间:mark-sweep。
在老生代收集器的管理分为两个阶段:
阶段1:遍历对象图,然后标记在使用的对象
阶段2:扫描整个内存,并且回收所有未标记的对象
网友评论