跟🤡杰哥一起学Flutter (十一、Flutter UI框架🦐聊)

🤡 不知不觉就写了10篇Flutter文章啦 🎉,重构版的实战项目也写了有一半了。常言道:温故而知新 ,本节决定回顾下之前写的章节,总结思考归纳 之余,做一些 延展学习 ,以便对Flutter这套 UI框架 有更深一步的理解。

Tips:本节概念性东西比较多,只需简单了解下,有个基本认知就阔以了,不看也没关系,不影响后续章节的学习😆

1. Flutter架构概述

Flutter 的本质是一套 UI框架 ,解决的是 一套代码在多端渲染高度优化的渲染管线 ,使其相比于RN、WebView等方案具有 更好的性能 。最直观的体现就是 绘制调用少一层

RN 等 JavaScript+原生渲染跨平台技术 需要 由其框架先调用Android框架 ,再通过 Skia 调用 GPU 进行绘制。而 Flutter框架 直接通过 Skia 即可调用 GPU 绘制 ,无需调用Android框架,调用步骤少一层,所以性能更接近于原生。

接着请出《八、进阶-异步编程速通🧨》里提到的 Flutter架构层次图

从上到下分为三层 ,上层组件依赖下层组件,组件间无法跨层访问,从上往下每一层的职责:

  • Framework (框架层)提供上层API的封装,如Widget、绘图、动画、手势等,用Dart语言编写。开发者直接接触的层,通过这些API来构建应用的界面和逻辑。
  • Engine (引擎层)提供Flutter核心API的底层实现 ,包括图形绘制、文本布局、文件和网络IO、插件架构和Dart运行时和编译环境等。使用C/C++编写,并通过 dart:ui 库暴露给上层调用。
  • Embedder (嵌入/平台层)与底层操作系统交互,将Engine层的内容渲染到不同平台设备上,同时处理平台相关的操作,如:访问相机、GPS、文件系统等,这一层的语言与平台相关,如Android是Java或Kotlin,iOS是Objective-C或Swift。

😁 复习完架构概述,接着了解下渲染管线的概念~

2. 渲染管线相关概念

2.1. 什么是渲染?

由计算机图形学的一个专业名称 "Rendering " 翻译而来,指的是 用软件从模型生成图像的过程 。在渲染过程中,计算机需要对三维模型或场景进行处理,包括建模、纹理、映射、光照计算、投影变换、视点变换等,最终生成一张 二维图像

😆 简单点说就是:计算机将存储在内存中的形状,转换成实际绘制在屏幕上的对应过程

2.2. 什么是渲染管线?

Rendering Pipeline ,也可译作 渲染流水线 ,指的是 将渲染的流程拆解成固定顺序 ,方便对流程进行优化,用较低的成本 用较低的成本 (时间、空间、计算能力) 将要显示的内容 高效渲染到屏幕上 。这个过程需要有CPU跟GPU协作完成,CPU 擅长 逐个指令计算GPU 更擅长 并行计算

2.3. 渲染管线的三个阶段

2.3.1. 应用程序阶段 (CPU)

这阶段最重要的输出是 渲染所需的几何信息(顶点数据) ,即 渲染图元,可以是点、线、三角面等,这些信息会传递给GPU的渲染管线处理,这个阶段进行的工作如下:

  • 准备场景数据:收集和准备场景中所有对象的数据,包括几何数据(顶点、索引)、材质属性、纹理、理、光照信息、摄像机参数等。将这些数据按照渲染顺序和属性进行排序,然后设置一些全局状态,如视图矩阵、投影矩阵等。
  • Culling剔除:确定哪些对象是可见的,那些不可见,常见的剔除技术有三类:背面剔除 (面向摄像机背面的三角形)、视锥体剔除 (不在摄像机视野内的对象)、遮挡剔除 (被其它对象完全遮挡的对象)。
  • 模型渲染状态设置:如使用的着色器程序、材质属性、光照模型、混合模式、深度测试和写入状态等。每个模型可能需要不同的渲染状态,该状态的设置通常涉及到对GPU状态的配置,所以需要在这个阶段为每个模型设置正确的渲染状态。

2.3.2. 几何阶段 (GPU)

也称 顶点处理阶段 ,或者理解成 顶点着色器 ,这个阶段的任务是 处理与几何相关的绘制,进行的工作如下:

  • 顶点变换将顶点从模型空间转换到视图世界和裁剪空间的过程,通常包括:视图矩阵、模型矩阵和投影矩阵的应用,这一步的目的是:将三维场景中的对象放置在正确的位置和方向,并考虑到摄像机的视角;
  • 光照 :可能需要进行一点光照计算,如顶点法线与光源方向的点积计算,以确定顶点的光照强度。为后续片段处理阶段提供必要的光照信息,以便片段着色器能够更准确地计算每个像素的颜色。
  • 裁剪将位于视图体积之外的顶点移除,视图体积定义了摄像机可以看到的空间,在这个空间外的顶点都不会被渲染,这一步减少了不必要的渲染工作量,提高了渲染效率。
  • 投影将三维空间的顶点映射到二维屏幕空间,根据使用的投影类型,顶点会被重新定位以模拟真实世界中的视觉效果。使用透视投影会模拟人眼对远近物体的感知,使得远处的物体看起来更小,使用正交投影则保持物体的实际大小,不随距离变化。
  • 屏幕映射 :将经过裁剪和投影处理后的顶点从裁剪空间转换到屏幕空间,顶点的坐标会被转换为屏幕上的像素坐标,通常是通过将坐标除以w分量 (透视除法) 并映射到屏幕的分辨率上,此时的顶点坐标同城在-1到1的范围内,还需要进一步转换为实际的屏幕像素坐标。

2.3.3. 光栅化阶段 (GPU)

这个阶段的任务是 将数据转换为可见像素,进行的工作如下:

  • 三角形设置:准备即将被光栅化三角形所需的信息,包括:顶点位置、纹理坐标、法线、颜色等。
  • 三角形遍历:确定三角形覆盖的像素范围,通常涉及三角形的边缘计算,并确定哪些像素位于三角形内部。
  • 像素/片元着色 :对光栅化后的每个像素执行 片段着色器 程序,计算像素的最终颜色,会应用到纹理映射、光照模型、阴影计算等,这是渲染过程中最耗时的部分之一,因为它需要对屏幕上每个像素都执行计算。
  • 混合 :将新渲染的像素颜色与帧缓冲区中已经存在的颜色进行合并,通常涉及这几个测试和操作:Alpha测试 (据像素的Alpha值决定是否丢弃该像素)、模板测试 (根据模板缓冲区中的值决定是否丢弃该像素,用于渲染复杂的场景,如透明物体或反射效果)、深度测试 (根据像素深度值Z值决定是否丢弃该像素,确保只有最近的像素才会被绘制,从而正确处理遮挡关系)、Alpha混合 (像素通过上述测试,它的颜色将与帧缓冲区对应位置的颜色混合,Alpha混合用于实现半透明效果,如玻璃或水)。

2.3.4. 将像素颜色输出到屏幕上

💁‍♂️ 光栅化完,渲染管线也算到头了,接着就是把最终的像素数据输出到屏幕上了:

  • 合成的颜色会被写入 帧缓冲区 (一片内存区域,用于存储即将显示屏幕上的图片数据);
  • 一旦 帧缓存区中的数据准备好 ,显卡会等待 VSync垂直同步信号 (一个用来协调显示器刷新率和显卡帧率,避免屏幕撕裂的同步机制);
  • 当VSync信号到来时 ,帧缓冲区中的内容会被发送到 显示器 ,显示器根据帧缓冲区中的数据 逐行扫描,将图像显示到屏幕上;
  • 整个过程是 连续 的,显示器会以一定的 刷新率(例如60Hz或144Hz)不断重复这个过程;
  • 当前帧显示到屏幕上后,渲染管线会开始处理下一帧数据,重复整个渲染流程;

💁‍♂️ 以上就是常规渲染管线的大概流程,简单点说就是:

CPU负责计算帧数据 ,算完交给 GPU栅格化处理和渲染 ,渲染完丢 图像缓冲区(显存) 存起来,在 合适的时机(VSync信号) 把图像缓冲区里的数据呈现到 Display(屏幕) 上。

💁‍♂️ 渲染管线的 核心渲染过程 (几何阶段+光栅化阶段) 一般是由 渲染引擎 来完成的。另外,CPU可以代替GPU进行图形渲染 ,只是效率没有GPU高,毕竟后者的 并行计算 能力使其能够快速将图形结果计算出来,并在屏幕的所有像素中显示,CPU和GPU绘图 这个视频很形象地演示了两者渲染的效果差异。

😁 关于渲染管线的概念就了解到这里,接下来看下 Android的渲染管线 是如何运作的~

3. Android 渲染管线浅析

3.1. 版本变更历史

早期的Android系统是没有 官方的 Vsync 机制 来协调GPU和显示器刷新,只有一个 双缓冲机制

绘制和显示器都有自己的缓冲区GPU后台缓冲区 渲染帧,完成一帧的渲染后,向系统发送一个信号,表明后台缓存区已经准备好可以渲染了。系统收到信号后,命令图形驱动程序 交换前后台缓冲区 (交换指针或引用) ,交换完成,前台缓冲区 就是 新渲染的帧,后台缓冲区则等待下一次的渲染,然后显示器从前台缓冲区读取数据来显示。

这种交换随时可能发生,如果显示器在读取缓冲区时,内容被改变了,就会导致 屏幕撕裂

因为没有Vsync机制,渲染帧中间是没有间隔的,一帧绘制完下一帧就开始被处理,也导致了 帧率的不稳定

Android 4.0 引入了 硬件加速Android 4.1 引入了 Vsync机制支持 + Triple Buffer (三重缓存) + Choreographer(编舞者),App渲染SurfaceFlinger 合成的时间点规范化,提供了稳定的帧率输出。

3.1.1. Vsync机制支持

Vsync信号 由屏幕显示设备产生,以固定的频率(如60Hz)发送给Android系统,这个信号确保了 GPU帧的生成速度屏幕刷新速度 保持一致,从而避免画面撕裂,当系统接收到Vsync信号时,会 立即处理下一帧数据

3.1.2. TripleBuffer (三重缓存)

理想情况下,16ms内,CPU处理完数据,GPU将数据渲染到后台缓冲区,渲染结束,等下一个Vsync与前台缓冲区交换数据,显示到屏幕上。

CPU和GPU串行执行任务,存在资源浪费,毕竟没有多余的缓存区用于处理数据,两者中的一个必然空闲。而且其中一个出问题,导致帧耗时超过16ms (如CPU 8ms + GPU 12ms),就会引起 掉帧 。三重缓存就是增加一个Buffer给CPU用 (即一个前缓冲区 + 两个后缓冲区),实现了CPU跟GPU的并行,保证了画面的连续性。

3.1.3. Choreographer (编舞者)

它在整个渲染链路中起着关键作用:

  • 承上接收和处理App的更新更新信息和回调等到Vsync信号到来时执行统一处理。如:Input事件、动画、Traversal(measure、layout、draw等操作)、判断卡顿掉帧、记录Callback耗时等;
  • 启下请求和接收Vsync信号

3.1.4. RenderNode & RenderThread

然后 Android 5.0 引入了两个较大的改变:

  • RenderNode :对 DisplayList 和 部分View的显示属性做了进一步的封装;
  • RenderThread渲染线程,负责执行所有的OpenGL命令,在RenderNode中存有渲染帧的所有信息,可以做一些View的异步渲染任务,减轻了UI线程的工作量。

3.1.5. Vulkan

接着 Android 7.0 引入了 Vulkan 的图形API,新一代图形API,采用了比OpenGL更加底层的架构,旨在提供更高效、更低开销的图形渲染。它是为多核处理、GPU协作和多GPU设计的,因此在现代硬件上的性能要由于OpenGL。然后有几个相关名词,也提一嘴:

  • OpenGL :一种用于绘制2D和3D图形的跨平台API,OpenGL ES 是它的子集,专为嵌入式设备(手机等)设计。
  • Skia :开源2D图形渲染库,能独立完成2D图形的绘制,并提供有限的3D效果支持。它主要通过 CPU软件绘制 实现,但也可以依赖底层的 GPU硬件加速技术,这由OpenGL、Vulkan、Metal等图形API提供支持。

3.2. 图形渲染架构概览

官网 给出了一张 Android渲染架构图

图像流图像流生产者Surface ,再被 图像流消费者 中的 SurfaceFlinger 消费掉,再到HAL(硬件抽象层) ,最后显示到 屏幕上。简单过下图中的几个组件:

  • 图像流生产者 :包括 MediaPlayer、CameraPreview、NDK(Skia)、OpenGL ES,前两者通过直接读取 图像源 来生成图像数据,后两者通过 自身的绘制能力 生成图像数据,它们通过 BufferData 的形式将图像数据传递到缓冲队列中。
  • 窗口管理器 :控制Window的系统服务 (AWS ),WindowsView的容器,每个窗口会关联一个 Surface ,对应 SurfaceFlinger 的一个 Layer (一个BufferQueue) ,AWS会管理这些窗口,并把它们的数据传递给 SurfaceFlinger
  • 图像流消费者SurfaceFlinger (主要) 或 显示OpenGL ES流 的应用,如相机App预览。SurfaceFlinger 会把系统中所有应用程序的最终 "绘制结果" 进行混合 (也可能交给HAL层的HWC)。
  • HAL抽象硬件层Hardware Composer 接收SurfaceFlinger提供的Layer完整列表进行合成,Gralloc 则封装了对 FrameBuffer(显存映像,写操作会立即反应在屏幕上) 的所有操作。

具体协作流程图如下:

其中 BufferQueue 的工作机制 (生产者消费者模式)

生产者:queue → 请求一块空闲的缓冲区;dequeue → 填充缓冲区后返回给队列;

消费者:acquire → 获得一块缓冲区;release → 使用完毕返回给队列;

😐 这部分简单概括下就是:

每个 Window 对应一个 Surface ,指向 SurfaceFlinger 的一个 Layer(BufferQueue) ,应用程序把绘制好的图像数据添加到 BufferQueueSurfaceFlinger 拿到数据后,请求HAL层决定Layer列表的由谁来合成,最终把合成后的图像数据显示到屏幕上。

3.3. 具体代码调用链路

😄 懒得画图了,感兴趣的直接看吧~

3.3.1. Activity初始化

App启动创建主线程消息循环后,创建第一个Activity时会调用 ActivityThread.performLaunchActivity() 方法内部通过 类加载器 创建 Activity实例

3.3.2. DecorView初始化

  • 接着调用 Activity.attach() ,内部创建了 PhoneWindow实例 并赋值给 Activity的成员变量 mWindow
  • 再接着 Activity.onCreate() 中调用 setContentView() , 最终调用 Window.getDecorView() 创建了 DecorView对象 且与 Window绑定。

3.3.3. ViewRootImpl、Choreographer 初始化

  • Activity执行完 onResume() 会调用 ActivityThread.handleResumeActivity() ,其中调用了 ViewManager.addView() ,最终调用 WindowManagerGlobal.addView() 初始化了 ViewRootImpl对象 ,而且和 DecorView 进行绑定,成为它的 parent 。而 Choreographer 也在 ViewRootImpl构造方法 中 完成了 初始化 ,而且通过 FrameDisplayEventReceiver.onVsync() 监听Vsync信号回调;

3.3.4. ViewRootImpl 与 WMS 建立联系

  • ViewRootImplDecorView管理者 ,负责 View Tree 的测量、布局和绘制,接着上面的流程往下走,会调用 ViewRootImpl.setView() ,其中通过 IWindowSession.addToDisplay() 以Binder远程调用的方式和 WMS 建立了联系。

3.3.5. ViewRootImpl 与 SurfaceFlinger 建立联系

  • 接着调用 WindowManagerService.addWindow() 创建 WindowState对象 ,并调用 win.attach() 来创建一个 SurfaceSession对象 ,其中调用了 android_view_SurfaceSession.cpp#nativeCreate() 构造了一个 SurfaceComposerClient对象 ,它是App与 SurfaceFlinger 沟通的桥梁。
  • 这个指针在第一次创建时会调用 createScopedConnection() 来创建一个 ISurfaceComposerClient对象SurfaceComposerClient 就是通过它来与 SurfaceFlinger 通信,除此之外,它还可以创建 Surface维护一个App所有的Layer(层)

3.3.6. requestLayout() 触发测绘

  • 经过上述步骤,ViewRootImplWMSSurfaceFlinger 都建立了连接,但此时 View 还没显示出阿里,回到 ViewRootImpl.setView() ,这里还调用了 requestLayout()。
  • 其中调用了 scheduleTraversals() 先设置同步屏障暂停处理后面的同步消息,然后调用 mChoreographer.postCallback (Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable , null),指定一个回调函数,在下一个vsync信号到来时,执行 mTraversalRunnable 里的 run() ,点开这个方法只有一句 doTraversal() ,移除同步屏障后调用 performTraversals() 正式进入 View的绘制流程

3.3.7. 测量与布局

  • 调用 relayoutWindow() 生成一个真正有效的 Surface对象(Native层) 并与 Java层的Surface对象 关联,同时会创建一个对应的 Layer ,然后依次调用 performMeasure (测量) → performLayout (布局),经过这两步已经确定了 每个View的大小和摆放位置

3.3.8. 具体绘制

  • 接着就是确定每个View的 具体绘制细节 ,调用 performDraw (绘制) → draw()drawSoftware()mSurface.lockCanvas (锁定画布) → 通过 nativeLockCanvas() 向Native层的Surface对象获取一个Canvas对象 ,然后将其传递给 DecorView → 调用它的 draw() 方法,实际上调用的 View#draw()dispatchDraw() 分配子元素的绘制,接着调用 onDrawForeground() 绘制前景。
  • lockCanvas() 中调用了一个JNI方法 dequeueBuffer() 获得了一块用于存储绘制元数据的 Graphic BufferSkia 会处理View的渲染,渲染数据输出到 GraphicBuffer 中。
  • 最后调用 unlockCanvasAndPost() 解锁当前Canvas,并将图形缓存区数据写入Layer的 BufferQueue

3.3.9. SurfaceFlinger合并Layer

  • SurfaceFlinger 检查到有新的 GraphicBuffer,会将这些缓冲区合成到一个完整的屏幕图像中,最后把合成后的图像渲染到屏幕上,渲染的同时还会处理输入事件,并将事件分发给对应的窗口。

🤡 以上就是Android渲染相关代码的大概调用链路,除了调用 requestLayout() 标记自身或子View需要重新布局外,还有下面的情况会触发 测绘

  • View 调用 invalidate() 标记自己为脏,需要重绘;
  • View Tree 中某些 View的状态发生变化,如大小、位置、可见性等;
  • 用户与设备交互,如触摸屏幕,导致事件分发和View状态的改变;

Android系统会尽量减少不必要的View遍历,如:视图的尺寸没有变化,系统可能会跳过measure和layout过程;只有一小部分View需要重绘,系统会尽可能只重绘这些脏区域。

4. Flutter 渲染管线浅析

🤫 看完Android,接着来看下Flutter的渲染管线又是如何运作的~

4.1. Embedder层

🤔 作为一个跨端UI框架,平台层 要做的事情,自然是 抽象底层平台差异,为开发者提供一个统一的开发环境,只需专注于应用程序的业务逻辑和用户界面设计,而不用关心底层平台的复杂性。在渲染管线中的作用就表现为:

将引擎层的Skia渲染输出与底层图形API相结合,以便在不同的平台上都能正确的显示渲染结果

那具体要做什么呢?

  • 渲染表面 :创建和管理一个平台特定的渲染表面,如 SurfaceViewCAEAGLLayer
  • 平台适配接口:提供与操作系统交互的接口,包括线程管理、输入事件处理等;
  • 图形API集成:封装底层图形API,如OpenGL ES、Metal,使得Skia可以方便地调用这些API;
  • 性能优化方案:根据不同平台的特性,提供性能优化方案;
  • 平台通道通信支持:确保平台通道可以安全地传递与渲染相关的信息,如渲染状态、渲染命令等;

4.2. Engine层

🤔 这一层的话,感觉渲染管线相关的核心工作就两个:

  • 向上提供dart:ui库 :提供一组Dart语言的API,包括但不限于绘制、布局、输入、图形、文字和动画等,RenderObject 通过这个库提供的API向Engince层发送绘制命令。该库还负责 向上传递用户输入事件,以便开发者可以响应用户的触摸、滑动等操作。
  • 集成Skia完成渲染相关工作 :当Engine层收到来自于dart:ui库的绘制命令,调用Skia将绘制操作 光栅化 将矢量图形转换为像素,将渲染结果保存到 帧缓冲区,然后调用底层的渲染API来绘制到屏幕上。

渲染管线的绝大部分操作都在这层完成,详细的渲染过程复杂得很,目前没应用场景,就不刨了,大概知道下~

4.3. Framework层

普通UI崽能接触到Flutter渲染管线相关的基本就在这一层了,先是 渲染三棵树 🌲,Flutter对 视图树(UI Tree) 的概念进行扩展,将 视图数据的组合与渲染 抽象成三个部分:

  • Widget (组件)Flutter UI 构建的基本单位 ,开发者使用Widgets来构建UI,它们是 不可变 的,每当应用状态发生改变时,Flutter 会构建一个新的Widgets树。
  • Element (元素)Widget 在UI树中的实例 ,Widgets树被编译成Element树,它们是 持久 的,即便Widgets树在状态变化时重新构建,Element 也可以保持不变。
  • RenderObject (渲染对象)负责实际的布局和绘制 ,Element树中每个Element都可能对应一个RenderObject,它会根据Element的配置进行 布局和绘制

然后是 布局(Layout)

根RenderObject 开始遍历(深度优先),每个RenderObject都会根据其子节点的大小和位置来计算自己的大小和位置。

接着是 绘制 (Paint)

RenderObject树中的每个RenderObject调用 dart:ui 中提供的API来绘制自己,绘制操作会被记录下来保存到图层(Layer) 中,层可以理解为屏幕上的一个 矩形区域,可以包含绘制内容或其它层,此时并没有实际绘制到屏幕上!

再接着是 合成 (Composite):

遍历Layer树,确定每个Layer的组合方式,并应用任何必要的视觉效果,如混合模式、裁剪等。

最后将 合成后的Layer树 被提交给Engine层的 Skia 进行光栅化,转换成实际的像素数据。

💁 以上就是Flutter渲染管线的大概流程,只是在概念层面做下了解,并没有深入源码。实际开发中,很少需要我们去干预渲染过程,大多数时间都是在Framework层堆Widget。

5. Flutter App 启动流程

main.dart 是Flutter App的 入口main() 函数是Dart应用程序的 起点 ,打开此文件的main()函数,只有一句runApp(const MyApp()) ,点进去看看runApp()做了啥:

5.1. WidgetsFlutterBinding

调用 WidgetsFlutterBinding.ensureInitialized() 并返回了一个 WidgetsBinding 实例,点进去方法看看:

判断 WidgetsBinding._instance 属性是否为空,空的话调用构造方法创建一个实例,然后返回WidgetsBinding.instance 属性调用:

checkInstance() 里的实例没初始化的话,会显示一个错误信息,吼,就获取一个 WidgetsBinding单例 。然后 WidgetsFlutterBinding() 并没有构造方法,调用的父类 BindingBase 的构造方法 (省略无关代码):

dart 复制代码
BindingBase() {
  // 省略代码...
  initInstances();
  initServiceExtensions();
}

initInstances() 用于 初始化绑定器实例initServiceExtensions() 用于 注册扩展服务 。Flutter App 运行在 Dart VM上,两者是可以互相调用的,如Flutter调用Dart VM的服务来获取内存信息、类信息、调用方法等,Dart VM也可以反过来调用 Flutter 层注册好的方法。不过它们间的调用要遵循 Json协议,只要注册过,名字匹配上就可以调用,注册方法 registerServiceExtension() ,具体代码示例如下:

dart 复制代码
// 两个参数分别为:服务名称 和 回调
void registerServiceExtension({
  required String name,
  required ServiceExtensionCallback callback,
}) {
  // 包装传递的服务名称
  final String methodName = 'ext.flutter.$name';
  // 将方法名和回调注册到VM中,这是一个native方法,developer是一个开发者包
  developer.registerExtension(methodName, (String method, Map<String, String> parameters) async {
    // 省略代码...
    late Map<String, dynamic> result;
    try {
      result = await callback(parameters);
    } catch (exception, stack) {
      // 省略代码...
    }
    result['type'] = '_extensionType';
    result['method'] = method;
    return developer.ServiceExtensionResponse.result(json.encode(result));
  });
}

Dart VM 和 Flutter 的 通信 遵循socket协议,只要连接上 虚拟机运行的URL 就行了,需要用到 vm_service 模块,Flutter App 主动连接 VM的代码示例如下:

dart 复制代码
// Service.getInfo 是 Flutter 提供的获取虚拟机服务URL的API
// 也可以通过 FlutterEngine 获取,但需要通过插件传递,不是很方便
Service.getInfo().then((value) {
  String url = value.serverUri.toString();
  Uri uri = Uri.parse(url);
  // convertToWebSocketUrl() 对 url进行转换,生成WebSocket能识别的url
  Uri socketUri = convertToWebSocketUrl(serviceProtocolUrl: uri);
  // Flutter与VM服务建立连接
  vmServiceConnectUri(socketUri.toString()).then((service) {
    // 调用Flutter注册的exit方法
    service.callServiceExtension('ext.flutter.exit',
        isolateId: Service.getIsolateID(Is.Isolate.current),
        args: {'enabled': true});
  });
});

// 附: Flutter App 注册退出方法
registerSignalServiceExtension(
  name: 'exit',
  callback: _exitApplication,
);
Future<void> _exitApplication() async {
  exit(0);
}

接着提一嘴 混入mixin 的语法,它的几个关键字:mixin (声明混入类)、with (使用混入类)、on (限制混入只能应用于特定的子类)。混入的实现是依靠 生成中间类 的方式,生成伪代码如下:

dart 复制代码
class D with A, B, C { 
  // D 类现在可以使用 A、B、C类的方法
}

// 生成的中间类(伪代码):
class _Intermediate1 extends A { }

class _Intermediate2 extends _Intermediate1 with B { }

class _Intermediate3 extends _Intermediate2 with C { }

class D extends _Intermediate3 { 
  // 可以添加自己的成员和方法
}

不难看出 混入是线性 的,优先级高于 继承 ,后面的混入类会覆盖前面的 同名方法,所以下面的代码:

dart 复制代码
mixin A { void printName() { print("A"); } }
mixin B { void printName() { print("B"); } }
mixin C { void printName() { print("C"); } }

class D with A,B,C {
  void printName() {super.printName(); }
}

void main(List<String> args) {
  D().printName();	// 输出:C
}

输出结果是 C ,如果想 每个混入类的同名方法都被调用,可以这样玩:

dart 复制代码
class Parent { void printName() { } }
mixin A on Parent { 
  void printName() {
    super.printName();
    print("A"); 
  }
}

mixin B on Parent { 
  void printName() {
    super.printName();
    print("B"); 
  }
}

mixin C on Parent { 
  void printName() {
    super.printName();
    print("C"); 
  }
}

class D extends Parent with A,B,C {
  void printName() {super.printName();}
}

void main(List<String> args) {
  D().printName();  // 输出:ABC
}

定义一个父类每个混入类用on限定只能被父类的子类混入方法中调用super使用混入的类继承父类 通过这四步就能实现多个混入类的 链式调用,每个mixin可以添加自己的逻辑,而不影响到其它mixin或基类。

看回代码,BingingBase 这里也是这样玩的,点开其中的两个混入类:

用on限定只有BindingBase的子类可以混入,然后 super.initInstances() ,就上面的玩法,综上:

BaseBinding 的构造方法起到 模板方法 的作用,定义了所有绑定类的初始化流程,通过 on + super 实现混入绑定类从前往后初始化。

接着阐述下各个Binding类的作用~

5.1.1. GestureBinding

Engine层事件监听的注册,在 handleEvent() 中处理所有RenderObject中注册的手势识别器决定分发给哪个组件,分发事件会回调 RenderObject中的 dispatchEvent()。

5.1.2. SchedulerBinding

任务调度,处理各种类型任务的调度实际,执行UI构建前后的一些任务,除此之外可以对任务进行优先级排序。

  • handleBeginFrame():执行scheduleFrameCallback() 注册的回调;
  • handleDrawFrame():执行 addPersistentFrameCallback/addPostFrameCallback 注册的回调;
  • addTimingsCallback():GPU光栅化耗时回调,可用于GPU耗时检测;
  • scheduleTask():优先级执行异步任务;'
  • scheduleFrameCallback():下一帧构建任务前的任务回调;
  • addPersistentFrameCallback():永久回调任务,下一帧之前都会执行一次;
  • addPostFrameCallback():addPersistentFrameCallback()后调用,只会被调用一次;
  • scheduleFrame():通知Engine有UI更新需要被回调;

5.1.3. ServicesBinding

注册管理平台服务:

  • _defaultBinaryMessenger:与platform通信;
  • handleSystemMessage:处理系统消息,如字体改变、内存不足;
  • _parseAppLifecycleMessage:生命状态回调处理;
  • RestorationManager:数据保存/恢复管理;

5.1.4. PaintingBinding

绘制库绑定,主要用于处理图片缓存;

5.1.5. SemanticsBinding

语义化层与Engine层的桥梁,主要是辅助功能的底层支持;

5.1.6. RendererBinding

PipelineOwner (用于管理RenderObject) 的管理,渲染树与Engine的桥梁,注册platform显示相关的监听,如:window.onMetricsChanged 、window.onTextScaleFactorChanged 等,创建了第一个RenderObject → RenderView。

5.1.7. WidgetsBinding

Widget三棵树的入口,Widget树与Engine的桥梁,处理Widget、Element之间的业务,提供一些生命周期回调,如:window.onLocaleChanged、onBuildScheduled 等。

5.2. scheduleAttachRootWidget

继续往下走,调用了 scheduleAttachRootWidget(binding.wrapWithDefaultView(app)) ,点开看看:

将一个Widget添加到渲染树的根节点上,wrapWithDefaultView() 定义在RendererBing中,用于创建一个默认视图,并将Widget包裹其中。View是用于显示Flutter内容的矩形区域,可以是整个屏幕,也可以是屏幕的一部分。

5.3. scheduleWarmUpFrame

SchedulerBinding 里的一个方法,触发一个空白帧,这个帧会通过渲染管道运行,但不会显示在屏幕上。这是为了初始化渲染管道,确保一切就绪,以便当第一个真正的帧被提交时,可以尽可能快地渲染。

参考文献

相关推荐
枫叶丹41 小时前
【HarmonyOS之旅】HarmonyOS开发基础知识(三)
华为od·华为·华为云·harmonyos
一个处女座的程序猿O(∩_∩)O1 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink4 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者6 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
SoraLuna6 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
向前看-6 小时前
验证码机制
前端·后端
燃先生._.7 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖8 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235248 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式