经过淘宝weex,微信,美团kmm,天猫Waft 等不同项目来了解现在各家公司在跨渠道方向上有哪些不同的项目,用了什么不同技能完结办法,然后在比照常用的react native ,flutter 和WebAssembly详细在技能上的差异在哪里。

image.png

客户端烘托履行逻辑

android

层次的底部是 Linux,它供给根本的体系功用,如进程办理,内存办理,设备办理,如:相机,键盘,显现器等内核处理的工作。体系结构第三个部分叫做Java虚拟机,是一种专门规划和优化的 Android Dalvik 虚拟机。运用程序结构层运用Java类形式的运用程序供给了许多的更高级其他服务。答应运用程序开发人员在其运用程序中运用这些服务。运用在最上层,即一切的 Android 运用程序。一般咱们编写的运用程序只被装置在这层。运用的例子如:浏览器,游戏等。

image.png

制造流程

  1. 创立视图

ui生成便是把代码中产生的view和在xml文件装备的view,经过measure,layout,dewa 构成一个完整的view树,并调用体系办法进行制造。Measure 用深度优先准则递归得到一切视图(View)的宽、高;Layout 用深度优先准则递归得到一切视图(View)的方位;到这里就得到view的在窗口中的布局。Draw 现在 Android 支撑了两种制造办法:软件制造(CPU)和硬件加速(GPU),会经过体系办法把要制造的view 组成到不同的缓冲区上

最初的ui装备

image.png

构建成内存中的view tree

image.png

2.视图布局

image.png

3.图层组成

SurfaceFlinger 把缓存 区数据烘托到屏幕,由于是两个不同的进程,所以运用 Android 的匿名同享内存 SharedClient 缓存需求显现的数据来达到意图。 SurfaceFlinger 把缓存区数据烘托到屏幕(流程如下图所示),首要是驱动层的工作,这 里不做太多解说。

image.png

4. 体系制造

image.png

制造进程首要是 CPU 预备数据,经过 Driver 层把数据交给 CPU 渲 染,其间 CPU 首要担任 Measure、Layout、Record、Execute 的数据核算工作,GPU 担任 Rasterization(栅格化)、烘托。由于图形 API 不答应 CPU 直接与 GPU 通讯,而是经过中心 的一个图形驱动层(Graphics Driver)来衔接这两部分。图形驱动保护了一个行列,CPU 把 display list 添加到行列中,GPU 从这个行列取出数据进行制造,终究才在显现屏上显现出来。

ios:

架构

image.png

  1. Core OS layer
  • 中心操作体系层包含内存办理、文件体系、电源办理以及一些其他的操作体系使命,直接和硬件设备进行交互
  1. Core Services layer
  • 中心服务层,咱们能够经过它来访问iOS的一些服务。包含: 定位,网络,数据 sql
  1. Media layer
  • 望文生义,媒体层能够在运用程序中运用各种媒体文件,进行音频与视频的录制,图形的制造,以及制造根底的动画效果。
  1. Cocoa Touch layer
  • 实质上来说它担任用户在iOS设备上的接触交互操作
  • 包含以下这些组件: Multi-Touch Events Core Motion Camera View Hierarchy Localization Alerts Web Views Image Picker Multi-Touch Controls.

ios 的视图树

image.png

ios的 制造流程:

image.png

image.png

image.png

显现逻辑

  • CoreAnimation提交会话,包含自己和子树(view hierarchy)的layout状况等;
  • RenderServer解析提交的子树状况,生成制造指令;
  • GPU履行制造指令;
  • 显现烘托后的数据;

提交流程

image.png

1、布局(Layout)

调用layoutSubviews办法; 调用addSubview:办法;

2、显现(Display)

经过drawRect制造视图; 制造string(字符串);

每个UIView都有CALayer,一起图层有一个像素存储空间,存放视图;调用-setNeedsDisplay的时分,仅会设置图层为dirty。 当一个视图第一次或许某部分需求更新的时分iOS体系总是会去恳求drawRect:办法。

以下是触发视图更新的一些操作:

  • 移动或删除视图
  • 经过将视图的hidden特点设置为NO
  • 翻滚消失的视图再次需求出现在屏幕上
  • 视图显式调用setNeedsDisplay或setNeedsDisplayInRect:办法

视图体系都会自动触发从头制造。关于自定义视图,就必须重写drawRect:办法去履行一切制造。视图第一次展现的时分,iOS体系会传递正方形区域来表明这个视图制造的区域。

在调用drawRect:办法之后,视图就会把自己符号为已更新,然后等候下一次视图更新被触发。

3、预备提交(Prepare)

解码图片; 图片格局转化;

4、提交(Commit)

打包layers并发送到烘托server;

递归提交子树的layers;

web :

web内容预备阶段

web 通常需求将所需求的html,css,js都下载下来,并进行解析履行后才进行烘托,然后是制造进程,先来看下前期工作

image.png

一个烘托流程会划分很多子阶段,整个处理进程叫烘托流水线,流水线可分为如下几个子阶段:构建DOM树、款式核算、布局阶段、分层、制造、分块、光栅化和组成。每个阶段都经过输入内容 –>处理进程–>输出内容三个部分。

  1. 烘托进程将HTML内容转化为能够读懂的DOM树结构

image.png

  1. 烘托引擎将CSS款式表转化为浏览器能够了解的styleSheets(生存CSSDOM树),核算出DOM节点的款式

image.png

styleSheets格局
image.png

  1. 创立布局树(LayoutTree),并核算元素的布局信息。

咱们有DOM树和DOM树中元素的款式,那么接下来就需求核算出DOM树中可见元素的几何方位,咱们把这个核算进程叫做布局。依据元素的可见信息构建出布局树。

image.png
4. 对布局树进行分层,并生成分层树(LayerTree)。

image.png

  1. 为每个图层生成制造列表,并将其提交到组成线程。

image.png

  1. 组成线程将图层分成图块,并在光栅化线程池中将图块转化成位图。

组成线程会依照视口附近的图块来优先生成位图,实践生成位图的操作是由栅格化来履行的。所谓栅格化,是指将图块转化为位图

image.png

image.png

  1. 组成线程发送制造图块指令DrawQuad给浏览器进程。浏览器进程依据DrawQuad音讯生成页面,并显现到显现器上

image.png

web 在android 中的制造

WebView实践上是一个ViewGroup,并将后端的详细完结笼统为WebViewProvider,而WebViewChromium正是一个供给依据Chromium的详细完结类。

再回到WebView的状况。当WebView部件产生内容更新时,例如页面加载结束,CSS动画,或许是翻滚、缩放操作导致页面内容更新,同样会在WebView触发invalidate办法,随后在视图体系的统筹安排下,WebView.onDraw办法会被调用,终究实践上调用了AwContents.onDraw办法,它会恳求对应的native端方针履行OnDraw办法,将页面的内容更新制造到WebView对应的Canvas上去。

draw()先得到一块Buffer,这块Buffer是由SurfaceFlinger担任办理的。

然后调用view的draw(canvas)当view draw完后,调用surface.java的unlockAndPostCanvas().

将包含有当时view内容的Buffer传给SurfaceFlinger,SurfaceFlinger将一切的Buffer混合后传给FrameBuffer.至此和native原有的view 烘托便是一样的了。

image.png

老练的结构的底层原理:

react :

RN 的 Android Bridge 和 IOS Bridge 是两头通讯的桥梁, 是由一个转译的桥梁完结的不同言语的通讯, 得以完结单靠 JS 就调用移动端原生 APi

架构

image.png

  • RN 的中心驱动力就来自 JS Engine, 咱们一切的 JS 代码都会经过 JS Engine 来编译履行, 包含 React 的 JSX 也离不开 JS Engine, JavaScript Core 是其间一种 JS 引擎, 还有 Google 的 V8 引擎, Mozilla 的 SpiderMonkey 引擎。
  • RN 是用类 XML 言语来表明结构, 用 StyleSheet 来规划款式, 可是 UI 控件调用的是 RN 里自己的两头完结控件(android 和 IOS)
  • JavaScript 在 RN 的效果便是给原生组件发送指令来完结 UI 烘托, 所以 JavaScript Core 是 RN 中的中心部分
  • RN 是不必 JS 引擎的 UI 烘托控件的, 可是会用到 JS 引擎的 DOM 操作办理能力来办理一切 UI 节点, 每次在写完 UI 组件代码后会交给 yoga 去做布局排版, 然后调用原生组件制造
  • bridge 担任js 和native的通讯,以android为例:Java层与Js层的bridge别离存有相同一份模块装备表,Java与Js互相通讯时,经过bridge里的装备表将所调用模块办法转为{moduleID,methodID,args}的形式传递给处理层,处理层经过bridge的模块装备表找到对应的办法履行,假如有callback,则回传给调用层

image.png

通讯机制

Java -> Js: Java经过注册表调用到CatalystInstance实例,经过jni,调用到 javascriptCore,传递给调用BatchedBridge.js,依据参数{moduleID,methodID}require相应Js模块履行。

Js -> Java: JS不自动传递数据调用Java。在需求调用调Java模块办法时,会把参数{moduleID,methodID}等数据存在MessageQueue中,等候Java的事情触发,再把MessageQueue中的{moduleID,methodID}回来给Java,再依据模块注册表找到相应模块处理。

事情循环

JS 开发者只需求开发各个组件方针,监听组件事情, 然后运用framework接口调用render办法烘托组件。

而实践上,JS 也是单线程事情循环,不管是 API调用, virtural DOM同步, 仍是体系事情监听, 都是异步事情,选用Observer(观察者)形式监听JAVA层事情, JAVA层会把JS 关怀的事情经过bridge直接运用javascriptCore的接口履行固定的脚本, 比方”requrire (test_module).test_methode(test_args)”。此时,UI main thread相当于work thread, 把体系事情或许用户事情往JS层抛,一起,JS 层也不断调用模块API或许UI组件 , 驱动JAVA层完结实践的View烘托。JS开发者只需求监听JS层framework定义的事情即可

react 的烘托流程

image.png

首要回顾一下当时Bridge的运转进程。当咱们写了相似下面的React源码。

<View style={{ backgroundColor: 'pink', width: 200, height: 200}}/>

JS thread会先对其序列化,构成下面一条音讯

UIManager.createView([343,"RCTView",31,{"backgroundColor":-16181,"width":200,"height":200}])

经过Bridge发到ShadowThread。Shadow Tread接收到这条信息后,先反序列化,构成Shadow tree,然后传给Yoga,构成原生布局信息。接着又经过Bridge传给UI thread。UI thread 拿到音讯后,同样先反序列化,然后依据所给布局信息,进行制造。

从上面进程能够看到三个线程的交互都是要经过Bridge,因而瓶颈也就在此。

image.png

首次烘托流程

  1. Native 打开 RN 页面
  2. JS 线程运转,Virtual DOM Tree 被创立
  3. JS 线程异步告诉 Shadow Thread 有节点改变
  4. Shadow Thread 创立 Shadow Tree
  5. Shadow Thread 核算布局,异步告诉 Main Thread 创立 Views
  6. Main Thread 处理 View 的创立,展现给用户

image.png

react native 新架构

image.png

  • JSI:JSI是Javascript Interface的缩写,一个用C++写成的轻量级结构,它效果便是经过JSI,JS方针能够直接取得C++方针(Host Objects)引证,并调用对应办法

其他JSI与React无关,能够用在任何JS 引擎(V8,Hermes)。有了JSI,JS和Native就能够直接通讯了,调用进程如下:JS->JSI->C++->ObjectC/Java

  • Fabric 是 UI Manager 的新名称, 将担任 Native UI 烘托, 和当时 Bridge 不同的是, 能够经过 JSI 导出自己的 Native 函数, 在 JS 层能够直接运用这些函数引证, 反过来 Native 能够直接调用 JS 层, 然后完结同步调用, 这带来更好的数据传输和功用提升

image.png

比照

image.png

flutter:

出产环境中 Dart 经过 AOT 编译成对应渠道的指令,一起 Flutter 依据跨渠道的 Skia 图形库自建了烘托引擎,最大程度地确保了跨渠道烘托的一致性

image.png

  • embedder: 能够称为嵌入器,这是和底层的操作体系进行交互的部分。由于flutter终究要将程序打包到对应的渠道中,关于Android渠道运用的是Java和C++,关于iOS和macOS渠道,运用的是Objective-C/Objective-C++。
  • engine:Flutter engine根本上运用C++写的。engine的存在是为了支撑Dart Framework的运转。它供给了Flutter的中心API,包含作图、文件操作、网络IO、dar运转时环境等中心功用。Flutter Engine线程的创立和办理是由embedder担任的。
  • Flutter framework: 这一层是用户编程的接口,咱们的运用程序需求和Flutter framework进行交互,终究构建出一个运用程序。

Flutter framework首要是运用dart言语来编写的。framework从下到上,咱们有最根底的foundational包,和构建在其上的 animation, painting和 gestures 。

再上面便是rendering层,rendering为咱们供给了动态构建可烘托方针树的办法,经过这些办法,咱们能够对布局进行处理。接着是widgets layer,它是rendering层中方针的组合,表明一个小挂件。

Widgets 了解

Widgets是Flutter中用户界面的根底。你在flutter界面中能够观察到的用户界面,都是Widgets。大的Widgets又是由一个个的小的Widgets组成,这样就构成了Widgets的层次依靠结构,在这种层次结构中,子Widgets能够同享父Widgets的上下文环境。在Flutter中一切皆可为Widget。

举例,这个Containerks 控件里的child,color,Text 都是Widget。

  color: Colors.blue,
  child: Row(
    children: [
      Image.network('http://www.flydean.com/1.png'),
      const Text('A'),
    ],
  ),
);

Widgets表明的是不可变的用户UI界面结构。尽管结构是不能够改变的,可是Widgets里面的状况是能够动态改变的。依据Widgets中是否包含状况,Widgets能够分为stateful和stateless widget,对应的类是StatefulWidget和StatelessWidget。

烘托和制造

烘托便是将上面咱们说到的widgets转化成用户肉眼能够感知的像素的进程。Flutter代码会直接被编译成运用 Skia 进行烘托的原生代码,然后提升烘托效率。

代码首要会构成widgets树如下,这些widget在build的进程中,会被转化为 element tree,其间ComponentElement是其他Element的容器,而RenderObjectElement是真正参加layout和烘托的element。。一个element和一个widget对应。然后依据elementrtree 中需求烘托的元素构成RenderTree ,flutter仅会从头烘托需求被从头制造的element,每次widget改变时element会比较前后两个widget,只有当某一个方位的Widget和新Widget不一致,才会从头创立Element和widget。 终究还会一个layer tree,表明制造的图层。

四棵树有各自的功用

image.png

Flutter制造流程

image.png

  • Animate,触发动画更新下一帧的值
  • Build,触发构建或改写 Widget Tree、Element Tree、RenderObject Tree
  • Layout,触发布局操作,确认布局巨细和方位信息
  • CompositeBits,更新需求组成的 Layer 层符号
  • Paint,触发 RenderObject Tree 的制造操作,构建 Layer Tree
  • Composite,触发 Layer Tree 发送到 Engine,生成 Engine LayerTree

在 UIThread 构建出四棵树,并在 Engine 生成 Scene,终究提交给 RasterThread,对 LayerTree 做光栅化组成上屏。

Flutter 烘托流程

image.png

  • UIThread

    UIThread 是 Platform 创立的子线程,DartVM Root Isolate 一切的 dart 代码都运转在该线程。阻塞 UIThread 会直接导致 Flutter 运用卡顿掉帧。

  • RasterThread

    RasterThread 本来叫做 GPUThread,也是 Platform 创立的子线程,但其实它是运转在 CPU 用于处理数据提交给 GPU,所以 Flutter 团队将其名字改为 Raster,表明它的效果是光栅化。

    C++ Engine 中的光栅化和组成进程运转在该线程。

  • C++ Engine 触发 Platform 注册 VSync 垂直信号回调,经过 Platform -> C++ Engine -> Dart Framework 触发整个制造流程

  • Dart Framework 构建出四棵树,Widget Tree、Element Tree、RenderObject Tree、Layer Tree,布局、记载制造区域及制造指令信息生成 flutter::LayerTree,并保存在 Scene 方针用以光栅化,这个进程运转在 UIThread

  • 经过 Flutter 自建引擎 Skia 进行光栅化和组成操作, 将 flutter::LayerTree 转化为 GPU 指令,并发送给 GPU 完结光栅化组成上屏显现操作,这个进程履行在 RasterThread

整个调度进程是出产者消费者模型,UIThread 担任出产 flutter::Layer Tree,RasterThread 担任消费 flutter::Layer Tree。

flutter 线程模型

image.png

Mobile渠道上面每一个Engine实例发动的时分会为UI,GPU,IO Runner各自创立一个新的线程。一切Engine实例同享同一个Platform Runner和线程。

  • Platform Task Runner

    Flutter Engine的主Task Runner,能够了解为是主线程,一个Flutter运用发动的时分会创立一个Engine实例,Engine创立的时分会创立一个线程供Platform Runner运用。改线程不仅仅处理与Engine交互,它还处理来自渠道的音讯。

  • UI Task Runner Thread(Dart Runner)

    UI Task Runner被Flutter Engine用于履行Dart root isolate代码,Root isolate运转运用的main code。担任触发构建或改写 Widget Tree、Element Tree、RenderObject Tree,生成终究的Layer Tree。

    Root Isolate仍是处理来自Native Plugins的音讯呼应,Timers,Microtasks和异步IO(isolate是有自己的内存和单线程操控的运转实体,isolate之间的内存在逻辑上是阻隔的)。

  • GPU Task Runner

    GPU Task Runner中的模块担任将Layer Tree供给的信息转化为实践的GPU指令,履行设备GPU相关的skia调用,转化相应渠道的制造办法,比方OpenGL, vulkan, metal等。GPU Task Runner一起也担任装备办理每一帧制造所需求的GPU资源

  • IO Task Runne

IO Runner的首要功用是从图片存储(比方磁盘)中读取压缩的图片格局,将图片数据进行处理为GPU Runner的烘托做好预备

Dart 是单线程的,可是选用了Event Loop 机制,也便是不断循环等候音讯到来并处理。在 Dart 中,实践上有两个行列,一个事情行列(Event Queue),另一个则是微使命行列(Microtask Queue)。在每一次事情循环中,Dart 总是先去第一个微使命行列中查询是否有可履行的使命,假如没有,才会处理后续的事情行列的流程。

image.png

isolate机制尽管 Dart 是依据单线程模型的,但为了进一步运用多核 CPU,将 CPU 密集型运算进行阻隔,Dart 也供给了多线程机制,即 Isolate。每个 Isolate 都有自己的 Event Loop 与 Queue,Isolate 之间不同享任何资源,只能依托音讯机制通讯,因而也就没有资源抢占问题。Isolate 经过发送管道(SendPort)完结音讯通讯机制。咱们能够在发动并发 Isolate 时将主 Isolate 的发送管道作为参数传给它,这样并发 Isolate 就能够在使命履行结束后运用这个发送管道给咱们发音讯。

假如需求在发动一个 Isolate 履行某项使命,Isolate 履行结束后,发送音讯奉告咱们。假如 Isolate 履行使命时,一起需求依靠主 Isolate 给它发送参数,履行结束后再发送履行效果给主 Isolate这样的双向通讯,让并发 Isolate 也回传一个发送管道即可。

weex:

架构:

image.png

  1. 将weex源码生成JS Bundle,由template、style 和 script等标签安排好的内容,经过转化器转化成JS Bundle
  2. 服务端布置JS Bundle ,将JS Bundle布置在服务器,当接收到终端(Web端、iOS端或Android端)的JS Bundle恳求,将JS Bundle下发给终端
  3. WEEX SDK初始化,初始化 JS 引擎,预备好 JS 履行环境
  4. 构建烘托指令树,Weex 里都运用 DOM API 把 Virtual DOM 转化成真实的Element 树,而是运用 JS Framework 里定义的一套 Weex DOM API 将操作转化成烘托指令发给客户端,构成客户端的真实控件
  5. 页面的 js 代码是运转在 js 线程上的,但是原生组件的制造、事情的捕获都产生在 UI 线程。在这两个线程之间的通讯用的是 callNative 和 callJS 这两个底层接口。callNative 是由客户端向 JS 履行环境中注入的接口,供给给 JS Framework 调用。callJS 是由 JS Framework 完结的,而且也注入到了履行环境中,供给给客户端调用。

烘托进程

Weex 里页面的烘托进程和浏览器的烘托进程相似,整体能够分为【创立前端组件】-> 【构建 Virtual DOM】->【生成“真实” DOM】->【发送烘托指令】->【制造原生 UI】这五个进程。前两个进程产生在前端结构中,第三和第四个进程在 JS Framework 中处理,终究一步是由原生烘托引擎完结的。 页面烘托的大致流程如下

image.png

各家项意图完结办法:

淘宝新⼀代⾃绘烘托引擎 的架构与实践

Weex 技能发展历程

image.png

Weex 2.0 简版架构

最上层的前端生态仍是没变的,应该仍是以vue的呼应式编程为主。

image.png

2.0多了js和c++的直接调用,减少js引擎和布局引擎的通讯开支。

image.png

image.png

Weex 2.0 重写了烘托层的完结,不再依靠体系 UI,改成依靠统一的图形库 Skia 自绘烘托,和 Flutter 原理很像,咱们也直接复用了 Flutter Engine 的部分代码。底层把 Weex 对接过的各种原生能力、三方扩展模块都原样接入。关于上层链路,理论上讲事务 JS 代码、Vue/Rax、JS Framework 都是不需求修改的。在 JS 引擎层面也做了一些优化,安卓上把 JavaScriptCore 换成了 QuickJS,用 bytecode 加速二次打开功用,而且结合 Weex js bundle 的特点做针对性的优化。

字节码编译原理

image.png

烘托原理

烘托引擎通用的烘托管线能够简化为【履行脚本】–>【构建节点】–>【布局/制造】–> 【组成/光栅化】–【上屏】这几个进程。Weex 里的节点构建逻辑首要在 JS 线程里履行,提交一颗静态节点树到 UI 线程,UI 线程核算布局和制造特点,生成 Layer Stack 提交到 GPU 线程。

image.png

天猫:WAFT:依据WebAssembly和Skia 的AIoT运用开发结构

整体计划

image.png

为什么挑选WebAssemy?

支撑 AOT 形式,拔高了功用上限;活泼的开源社区,降低项目推动的危险;支撑多言语,拓宽开发者集体。

WebAssembly(又名wasm)是一种高效的,初级其他编程言语。 它让咱们能够运用JavaScript以外的言语(例如C,C ++,Rust或其他)编写程序,然后将其编译成WebAssembly,从而生成一个加载和履行速度非常快的Web运用程序

WebAssembly是依据仓库的虚拟机的二进制指令格局,它被规划为编程言语的可移植编译方针。现在很多言语都已经将 WebAssembly 作为它的编译方针了。

image.png

waft 开发办法

能够看到是选用类前端的开发办法,限制一部分css能力。终究编译为WebAssembly,打包成wasm bundle。 在进行aot 编译城不同架构下的机器码。

image.png

运转流程

能够看到bundle 加载进程中,会履行UI区域的不同的生命周期函数。然后在烘托进程则是从virtual dom tree 转化到widget tree,然后直接经过skia 烘托库直接进行烘托。

image.png

Waft 第二阶段效果-跨渠道

image.png

美团KMM在餐饮SaaS中的探索与实践

KMP:Kotlin Multiplatform projects 运用一份kotlin 代码在不同渠道上运转

KMM:Kotlin MultiplatformMobile 一个用于跨渠道移动运用程序的 SDK。运用 KMM,能够构建多渠道移动运用程序并在 Android 和 iOS 运用程序之间同享中心层和事务逻辑等方面。开发人员能够运用单一代码库并经过同享数据层和事务逻辑来完结功用。其实便是把一份逻辑代码编译为多个渠道的产品编译中心产品,在不同渠道的边际后端下转为不同的变异后端产品,在不同渠道下运转。

image.png

IR 全称是 intermediate representation,表明编译进程中的中心信息,由编译器前端对源码分析后得到,随后会输入到后端进一步编译为机器码

IR 能够有一系列的表现办法,由高层表明逐渐下降(lowering)到低层

咱们所讨论的 Kotlin IR 是笼统语法树结构(AST),是比较高层的 IR 表明类型。

有了齐备的 IR,就能够运用不同的 后端,编出不同的方针代码,比方 JVM 的字节码,或许运转在 iOS 的机器码,这样就达到了跨端的意图

image.png

比照

image.png

总结

当时存在4种多端计划:

  1. Web 容器计划
  2. 泛 Web 容器计划
  3. 自绘引擎计划
  4. 开放式跨端结构

image.png

引证文章:

zhuanlan.zhihu.com/p/20259704​​

zhuanlan.zhihu.com/p/281238593​​

zhuanlan.zhihu.com/p/388681402​​

/post/708412…​​

guoshuyu.cn/home/wx/Flu…​​

oldbird.run/flutter/u11…​​

w4lle.com/2020/11/09/…​​

blog.51cto.com/jdsjlzx/568…​​

www.devio.org/2021/01/10/…​​

gityuan.com/flutter/​​

gityuan.com/2019/06/15/…​​

zhuanlan.zhihu.com/p/78758247​​

www.finclip.com/news/f/5035…​​