一、Flutter技术简介与适用场景概要
Flutter技术简介
Flutter技术的基本原理
- Flutter是一种自绘渲染引擎,分三层:基于Dart的Framework层、基于C或C++的渲染引擎层和基于不同平台的平台相关实现层。
-
在Framework层,Flutter提供了相关的UI组件以及动画、Canvas绘制和手势等基本能力,保证了上层UI描述的标准一致。
-
在渲染引擎层,包含了渲染管线、Dart虚拟机以及与平台实现层相关的协议。
-
在平台相关实现层,Flutter会针对与渲染引擎层约定好的接口进行对应平台的实现,常见不同平台的RunLoop、Thread以及Surface绘制相关的实现差异会在该层中体现。
-
Flutter配套的Dart虚拟机支持JIT与AOT等多种编译方式,这也保证了在开发模式下,Dart虚拟机可以实时加载JIT编译产物,具备代码热加载的能力,帮助研发人员快速调试;而在正式发布时,基于AOT编译的产物具备优异的性能,从而保证了Flutter在生产环境中具备高性能。
-
Flutter的渲染部分对接了名叫Skia的跨平台图形库,它保证了在不同平台渲染的强一致性,这也是Flutter称为自绘渲染引擎的原因。但也有问题,这种方案在与原生控件的标准对齐时,总是需要自行地在Dart Framework进行具体的能力实现,这就使得完全遵循平台交互视觉标准时,会增加额外工作量。
二、构建基于Flutter的混合应用
Flutter工程和构建
工程结构
Flutter App
-
运行命令:
flutter create -t app test_app
-
核心工程结构
├── README.md
├── android
├── ios
├── lib
│ └── main.dart
├── pubspec.yaml
├── test
│ └── widget_test.dart
└── web
└── index.html
- 主要特点
-
结构固定,相对位置基本固定
-
Native视角不友好,在默认情况下,Native工程源码无法独立运行,需要嵌入Flutter的工程结构中
-
只有源码依赖,没有默认的产物依赖机制
-
Flutter Module
-
运行命令:
flutter create -t=module test_Module
-
核心工程结构
├── .android
├── .ios
├── README.md
├── lib
│ └── main.dart
├── pubspec.lock
├── pubspec.yaml
├── test
│ └── widget_test.dart
- 如果Android工程要依赖Flutter工程,需要在主工程的
settings.gradle
中添加如下代码
evaluate(new
File(settingsDir.parentFile,
'my_flutter/.android/include_flutter.groovy'
))
- iOS则在主工程Podfile中添加如下依赖
flutter_application_path = '../my_flutter'
load File.join(flutter_application_path,'.ios','Flutter','podhelper.rb'
- 主要特点
-
主工程可以非常方便地直接依赖Flutter工程
-
将依赖分成源码依赖和产物依赖,更贴合工程实践的需要
-
但是弱化了Flutter视角
-
构建
Flutter整包构建
-
基于Flutter APP则可直接Flutter build
-
基于Flutte Module则比较复杂
Flutter产物构建
- 核心思路是通过壳工程的方式,将Flutter产物的各部分复制到壳工程中,然后将壳工程打包并上传
- Android和iOS壳工程打包核心过程如下
Android
iOS
- 用以下命令可以构建Flutter和对应的Flutter的所有产物,并且生成相应的依赖文件
Flutter build aar // android
Flutter build ios-framework // ios
混合架构下的架构设计与应用
混合架构下的页面管理
-
Flutter技术主要由c++实现的FLutterEngine和Dart实现的Framework组成
-
FlutterEngine负责线程管理、Dart VM状态管理和Dart代码加载等工作
-
Dart代码所实现的Framework则是业务接触到的主要API,诸如Widget等概念就是在Dart层面的Framework内容
-
虽然一个进程最多只会初始化一个Dart VM,但是一个进程可以有多个Flutter引擎,多个引擎实例共享同一个Dart VM
-
由于一个进程可以有多个引擎,因此页面管理的解决方案可以分为多引擎解决方案和单引擎解决方案
多引擎解决方案
-
基本设计思路:应用中会存在多个引擎,每次当从Native页面打开一个Flutter页面时,便重新创建一个新的引擎实例供Flutter页面使用
-
优势:实现简单、开箱即用
-
缺点
-
资源开销呈线性增长,导致应用内存较大
-
与引擎对应的Isolate是相互独立的,它们之间的资源是隔离的,虽然在一个Isolate中无法读取另一个Isolate中的全局变量,但是页面间共享的全局数据却是常见的需求,这就需要额外地引入某些机制,做到数据共享,这使得某些需要实现页面间数据共享的需求变得非常复杂
-
单引擎解决方案:共享视图
-
FlutterBoost
-
单引擎解决方案指整个应用只存在一个引擎,所以Flutter页面共享该引擎
-
相比多引擎解决方案,单引擎解决方案天生不存在环境隔离问题,并且资源开销也少
混合架构下的平台复用能力
Platform Channel
- Flutter定义了三种Channel,分别用于传递字符串和半结构化信息的BasicMessageChannel、传递方法调用的MethodChannel和用于数据流通信的EventChannel
-
每种Channel均有三个重要的成员变量
-
String类型的name,代表Channel的名字,也是唯一的标识码
-
BinaryMessenger类型的messager,代表消息信使,是消息的发送与接受的工具
-
MessageCode类型或MethodCodec类型的Coddec,代表消息的编解码器
-
-
BinaryMessenger是Platform端与Flutter端通信的工具,其通信使用的消息格式为二进制格式数据
-
当初始化一个 Channel,并向该 Channel注册处理消息的 Handler 时,实际上会生成一个与之对应的BinaryMessageHandler,并以channel name为Key,注册到 BinaryMessenger 中。 当 Flutter 端将消息发送到 BinaryMessenger 时,BinaryMessenger 会根据其入参 Channel 找到对应的BinaryMessageHandler,并交由其处理。
-
BinaryMessenger并不知道 Channel 的 存 在,它只和BinaryMessageHandler打交道
-
Channel 和 BinaryMessageHandler 则是一一对应的
-
由于Channel BinaryMessageHandler接收到的消息是二进制格式数据,无法直接使用,故 Channel会将该二进制消息通过 Codec解码为能识别的消息,并传递给Handler处理
-
当Handler处理完消息之后,会通过回调函数返回result,并将result通过编解码器编码为二进制格式数据,通过BinaryMessenger发送回Flutter端
-
-
注意
-
Platform侧的代码运行在主线程。FlutterEngine自己不创建线程,其线程的创建与管理是由Embedder提供的,并且FlutterEngine要求Embedder提供四个Task Runner,分别为Platform Task Runner、UI Task Runner、GPU Task Runner和 I/O Task Runner。Platform侧的代码运行在UI Task Runner中、在Android和ios上,Platform Task Runner运行在主线程上。因此不应该在Platform端的Handler中处理耗时操作。
-
Platf Channel并非是线程安全的。FlutterEngine中多个组件是非线程安全,故与FlutterEngine的所有交互必须发生在Platform Thread。故将Platform端的消息处理结果回传到Flutter端时,需要确保回调函数是在Platform Thread中执行的
-
Texture
- 有时需要在Flutter页面上显示纹理,比如需要在Flutter页面播放视频,或者在Flutter页面上展示拍摄预览界面等,Flutter提供了用于视频播放的video_player和用于拍摄的camera,这两个插件都是使用名为Texture的Widger展示纹理的
- Texture使用只需传入一个textureId即可,其大小完全由父组件决定。如果获得一个Texture的texture的textureId,并且知道其显示的宽和高就可以轻松显示到界面上
SizedBox (
child: Texture(textureId: textureId),
width: 100,
height: 100,
)
- Texture中的textureId对应的是平台端的一个后端纹理,这个textureId是由TextureRegistry生成的
public class TexturePlugin implements FlutterPlugin {
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
TextureRegistry textureRegistry = binding.getTextureRegistry();
TextureRegistry.SurfaceTextureEntry entry == textureRegistry.createSurfaceTexture();
//获取 textureId,可通过 Platform Channel 传递给 Texture
long textureId = entry.id();
//获取 surfaceTexture,用于渲染内容
SurfaceTexture surfaceTexture = entry.surfaceTexture();
}
}
PlatformView
-
一些第三方View的渲染通常是集成在SDK内部的,由于无法修改它的渲染目标,所有更无法使用外接纹理,所以Flutter官方提出了PlatformView
-
对Flutter界面上的任意一个元素,最终都需要一个GPU纹理的载体来保证SKia可以顺利地将他在屏幕上绘制出来
-
对于一个Native的View,需要怎样将它转换成一个纹理
- 在Android中,通过VirtualDisplay这个API可以将一个AndroidView的内容投影到一个SurfaceTexture上,然后SurfaceTexture绑定的纹理就存储了当前AndroidView上呈现的内容,但是这一种昂贵的方案
三、多场景应用架构和设计
Flutter编程模型分析和实践
架构设计的第一性原理
关于“概念抽象”的几个推荐原则
-
奥卡姆剃刀
- 在能够解决问题的前提下,概念越少越好
-
相互独立、完全穷尽(MECE)
- 将问题抽象分解为几个概念之后,概念之间有清晰的边界,相互没有重叠
- 这几个概念组合之后能够还原问题,没有遗漏
-
保守性创新
- 在原有概念基础上,“走一小步”做创新
- 因为概念是架构设计中连接设计者和实施者之间的重要纽带,双方拥有的“共识”越多越好
- 不宜提出完全创新的概念,避免实施者不能判断概念的有效性而产生抵触,更不能偷换公共概念的内涵,这会导致效果上适得其反
-
优先做“合题”
- 面对问题,抽象出概念
- 抽象出概念,这是 “正题”
- 出现了新问题,不能归类到原概念,对原概念抽象提出了跳转,这是 “反题”
- 通过抽象升级,更新概念,让它能兼容新老问题,这是 “合题”
关于“行为模式设定”的几个推荐原则
-
单一职责
- 给行动节点设定明确的唯一目标
-
有限周期
- 设定生命周期是时间维度的体现,非常自然,易于理解
-
单向链条
- 要有核心的逻辑链条,方向清晰,分支越少越好
Flutter编程模型分析
-
对于GUI框架,数据是程序表象的底层,围绕数据处理抽象出Model概念,围绕界面构建抽象出View概念,最后通过行为模式链接
-
和GUI框架相比,Flutter最大的差异是行为链接的行为差异化构建,按照时间先后,依次经过Controller、Presenter、ViewModel和Store(Flux)等链接概念
-
Controller
- 作为最初解决方案,职责不清,周期不明,扩展混乱,只是混沌的打包
-
Presenter
- 明确了职责,隔离View和Model,所以确定了行为的方向,有不错的生命力
-
ViewModel
- 具体地定义了行为的模式,即双向绑定,明确且清晰
-
Flux
- 大幅简化了行为模式,即单向数据流,悉能为可以用一个最简的函数表示(ui = f(data)),极其简洁
-
Flutter基础编程模型
-
Flutter在行为链接方面选择Flux方向,因为Model和View之间的行为已经极简地表达为一个函数了,感觉就像消失了一样。
-
StatefulWidget是Flutter应用架构设计的原点。Flutter应用架构设计在本质上就是从两个维度对StatefulWidget赋能,赋予StatefukWidget状态分治的能力和StatefulWidget之间信息通信的能力
- StatelessWidget:和一个纯函数是等价的,本质上是一个构建UI的纯函数。当StatelessWidget变复杂时,拆分时非常简单安全的
- StatefulWidget:拥有运行时数据(也就是状态),需要管理数据(状态管理)。当复杂度上升时,会面临两个问题
- 向内看,如何对StatefulWidget自身膨胀的复杂度进行拆分
- 向外看,如何在多个StatefulWidget之间做数据(状态)共享
-
StatefulWidget 状态分治的能力
- StatefulWidget可以简化地看成是Model+StatefulWidget
- Model的构成是数据和处理数据的逻辑,在另一些语境下,也叫状态和处理状态的函数,接下来对Model做分治设计
-
数据不拆、拆分逻辑
- 特点是全局共用一个数据结构体对象,所有状态全部放在对象中,业务习惯叫“统一状态管理”
-
逻辑不拆,拆分数据
- 也称“步进状态管理”,类似于状态机模式
-
同时拆分逻辑和数据
- 在更细的粒度上,将数据和它对应的处理逻辑拆分成包,变成更小的域,然后统一协调这些子域,也称“组合状态管理”
-
- StatefulWidget之间信息通信的能力
-
Flutter将StatefulWidget组织成树形结构,在StatefulWidget之间通信的结构有两种
- 通过全局域内的单例对象实现通信,这就是很常见的EventBus思路
- 通过共同父节点实现通信,Flutter提供的InheritdWidget支持这种方式,但是应该避免这种方式引起不必要的Widget刷新
-
对于StatefulWidget之间的通信方式,可归类为Notify模式、Transfer模式和Invoke模式
- Notify模式:通知/监听模式,Flutter提供了ValueNotiifier和ChangeNotifier,简单方便,适用于轻量信息通信
- Transfer模式:数据传输模式,Dart提供了Stream支持这种模式,它的通信是数据传输,类似于Socket。
- Invoke模式:接口调用模式,但很少使用,因为有些重,优势是双向的
-
Flutter 应用编程模型
流式场景下的架构设计与应用
流式页面容器架构设计
- 在流式页面容器架构设计过程中,面对实际的业务场景,通过以下几方面解决端到端的流式页面容器架构设计
-
在搭建平台侧,实现页面搭建、组件管理、协议编排等能力,与投放平台、A/B实验平台和监控平台打通
-
在客户端侧,采用MVVM模型,设计通用的时间协议,抽象通用的页面布局、数据管理及事件处理能力,减少重复代码生成,提升研发效率。在页面布局管理方面,与列表容器PowerScrollView深度结合,实现高效的页面渲染、数据驱动的页面刷新能力
-
使用DinamicX作为DSL,实现动态模板渲染,满足投放以及运营需求
-
在与服务端通信协议方面,借助FaaS的能力,定义一套云端一体化的事件协议,解决业务逻辑动态化的问题,减少发版依赖,进而提升交付效率
-
协议的设计
-
在通信协议设计上,全部采用事件传递方式,包括客户端与服务端、组件与组件、页面与组件、页面与APP之间
-
这也是云端一体化的设计,理论上开发者只需要考虑事件的发送与接收,具体事件的处理在客户端还是在服务端,由对应的Handler决定
-
在云端一体化的设计下,事件的处理更加灵活,可以更方便地将逻辑后移,当业务发生变更时,减少对发版的依赖
事件中心的设计
- 在PowerContainer的设计中,一切皆是事件(Event):无论是数据更新、消息传递、网络请求、服务端返回的结果、还是自定义的本地处理逻辑
数据中心的设计
- 在MVVM架构中,数据中心承担着ViewModel的角色,处理Update事件,主要负责数据的更新及UI视图的刷新
- 在UI渲染方面,闲鱼将列表容器PowerScrollView与动态模板渲染DXFlutter相结合,实现页面渲染及数据更新后的页面刷新能力
列表容器
- PowerScrollView是闲鱼实现的一套功能完善、高性能的列表布局容器,满足了页面容器对于瀑布流、卡片曝光、锚点定位等能力的需求。在视图渲染刷新方面,PowerScrollView提供了列表的局部刷新能力,完美解决了数据更新后试图的刷新问题
动态模板渲染
- DXFlutter使用DinamicX作为DSL,在FLutter端实现了高效的动态模板渲染能力。闲鱼使用DXFlutter实现了Component层协议的动态模板渲染
- 在DSL中,闲鱼自定义了页面容器PowerContainer的事件powerEvent,通过它可以生成页面容器的通用事件类型,将DinamicX卡片事件与页面容器事件中心打通。
- 每个Conponent生成一个唯一的ComponentKey,根据SectionKey+ComponentKey生成卡片的唯一标识,来确认要操作的是哪一份数据
Section状态管理
- 在协议的设计方面,每个Section定义了state,在事件中心处理Remote请求事件和应答事件,会更新Section的state
- 通过注册render handler,针对Section的不同状态返回加载状态Widget
Tab容器支持
- 闲鱼在Section的协议中引入了Slot的概念,当搭建页面时,会指定Tab容器的Slot Section,默认不展示任何信息的空插槽
- 当每次切换Tab容器时,通过Replace事件修改页面容器的Section信息
Flutter场景下的多媒体架构实践
基本概念
- 纹理是承载计算机数据的重要概念。可以说屏幕上看到的所有东西,不管是文字还是图片,他们的最终载体都是纹理。它代表着GPU里的一块内存数据,通过一些图形API,程序可以对其进行读取和编辑等操作
- 通过PlatformChannel和Dart FFI两种思路,Dart可以调用Native端原生的系统API
PlatformChannel
- PlatformChannel是一种异步的消息传递通道,Dart端和Native端通过Channel可以实现消息的传递。进而实现调用对应的方法
- Flutter提供了PlatFormChannel:BasicMesssageChannel、MethodChannel和EventChannel,用来支持和Native之间数据的传递
- 通常使用MethodChannel实现API的调用
- Channel本身有数据的拷贝操作,所以不适合传输图片等大量数据,但是它可以用作数据指针地址的传输
Dart FFI
- Dart FFI:Dart提供的一种跨越运行时边界访问(C/C++)代码的机制
- 相比于Channel的机制,FFI在调用效率上提高了一个量级
- 缺陷
- 易用性:FFI只能调用C/C++代码,所以在Native端,还需要写一层与平台相关的封装层
- **回调:**在Flutter中,由于Dart编译的机器码运行在UI线程中,所以当Dart调用Native代码时,如果有耗时操作,则会阻塞UI线程,造成页面卡顿。但是如果使用异步回调机制,则因为Dart本身的垃圾回收机制,回调函数运行时因为变量的生命周期问题会出现异常错误,所以Dart FFI调用是不支持异步回调的
外接纹理
- Flutter引擎层的一个重要概念是Layer Tree,它是Dart Runtime输出的一个树形数据结构。树上的每一个叶子节点代表了一个界面元素。
- 每一个叶子节点代表了Dart代码排版的一个控件
Platform线程和EGLContext
- 在音视频的生产端,通常需要非常多的视频处理逻辑,比如滤镜、贴纸、挂件和专场等,这些处理逻辑基本上都是基于OpenGL的
- 在FlutterEngine内部,EGLContext也是在Platform线程上创建的,Flutter也会将Platform线程的Current Context设置成它创建的EGLContext,并且EGLContext也会被设置成GPU线程的Current Context
- 也就是说,在FlutterEngine里,主线程和GPU线程其实共用一个EGLContext
- 在混合栈的实践过程中,在创建和销毁Flutter界面时,也可能伴随着FlutterEngine内资源的创建和销毁
- 通常来说,一个设计优秀的视频发布器会将所有的处理逻辑运行在自己创建的线程里,主线程不会有OpenGL操作
- 当在Flutter混合环境中开发音视频、地图或游戏等产品时,需特别注意不能在主线程操作OpenGL,或者说当操作OpenGL时,需要在逻辑开始的地方抢占主线程的Current Context
云端一体化的架构设计与应用
- 无服务架构(Serverless)被誉为下一代云计算,自概念推出以来,因为能带来研发交付速度提升与成本的降低,在业内引起巨大反响
一体化设计研究
研发模式演进
三端语言一体化
- Serverless由BaaS(Backend as a Service)与FaaS(Function as a Service)两部分组成
- BaaS主要包括数据库存储、消息队列等,针对复杂的需求,建议由服务端BaaS层封装领域服务供FaaS层使用
通信一体化
- 以往客户端将每一次的端上改动和当前数据都以Request的形式发送给服务端,由服务端算出在该操作下新的价格应该是多少,并以Response的形式返回给客户端,双方的一纸约定仅仅靠一个JSON格式文件
- 但是我们希望客户端研发人员写FaaS层代码时尽量不要有明显的感知,而FaaS层里实际上封装的是一个个函数,其实客户端上也有一些操作可以理解为一个个函数
- 将双端的能力函数化后,可以设计如下图的一体化通信方式
- 通过一体化框架屏蔽通信细节,前后端用一份State,降低了协议转换成本,前端研发人员在调用FaaS层服务时,如同调用本地函数一样简单
编程模型一体化
- Flux定义了一套单向数据流的原则,在Flutter一体化下,基于前后端一体化的整体考虑,设计了一体化编程模型
-
它总包含三个模块:Render、Converter、Model
-
其中只有Render部分在客户端上,页面上的渲染数据全部来自FaaS层转化的ViewModel(State)
-
当界面上有交互发生要改变State时,就将事件(Event),最后路由到Model层进行处理
-
Model层根据这个事件,可能会从后端领域拉取原始业务数据,然后由框架将数据交给Converter转成渲染所需要的State
云端一体化架构升级
工程一体化
- 闲鱼通过将FaaS层业务代码和Flutter业务代码放到一个工程目录下,且都单向依赖通用代码,Flutter主工程再以git依赖的方式将工程引到工程里,实现工程一体化的工作
- 客户端通过Flutter的Tree Shaking机制,把FaaS层代码与其引用的Library拆剪掉。FaaS层代码基于Dart开发,通过源码依赖分析,生成部署产出物时拆剪掉Flutter代码,从而实现一体化工程的选择性编译与部署。
工程一体化RPC通信机制
- 下单页的代码采用的是事件驱动的方式,得益于三端语言一体化,通过实现类RPC机制,双端以函数调用的形式通信
- 服务端的RPC框架是基于Java反射实现的,由于Flutter无法实现反射,所以通过Dart注解实现RPC机制
- 当研发人员需要获取服务端数据时,只需定义接口与实现FaaS层的业务逻辑即可
- 客户端的请求与服务端的请求处理的模板代码都由工具自动生成,客户端请求代码根据注解提取,包括接口信息、接口版本、请求参数和响应数据类型等,服务端代码通过自定义脚本输出服务端FaaS格式的框架与目录结构
一体化架构设计
-
FaaS层需要支持Dart Runtime环境,此处分为四层
-
provide pub适配多个Serverless平台
-
上层的Dart Runtime是基于官方的源码编译的,尽量不做改动
-
Proxy RSocket是中间件调用层,闲鱼实现了调用各自中间件的插件机制
-
最上层是业务最小依赖的代码业务pub
-
-
服务端已经有很多非常强大的中台能力,如果一体化无法复用原有服务端的能力,开发成本会比较大,则通过实现JProxy框架提供Dart调用Java中间件的能力
-
三端语言一体化、通信一体化、编程模型一体化、工程一体化与RPC机制都属于应用框架层
云端一体化研发模式思考
前后端分工
- FaaS层参考了BFF层的定义,称为SFU层,因为这一层是客户端研发人员为了用户而写的,而不是为了另一个技术栈的同事写的;这一层负责聚合、裁剪和结构化各领域数据,服务于客户端本身,承载多变的能力;而服务端研发人员会更加专注领域建设,提供领域服务
成本
- 相比于“Flutter + 服务端”的研发模式,一体化的研发模式抹消了前后端接口约定、Mock数据开发、编写前后端重复代码、前后端联调、定位问题时前后端转交Bug、遇到问题前后端沟通修改接口协议等环节,主要是协作沟通的成本
- 问题是,客户端研发人员需要学习业务领域接口,熟悉各个接口的QPS、RT和降级策略,并做好业务整体技术方案的设计