跨平台技术方案选择Flutter总结
1、移动端的跨端技术选型
移动端的跨端技术方案,我们关注的点主要集中在这几个方面
- 研发效率:降低开发成本,最大的代码复用,可实现版本的快速迭代。
- 多端一致性:好的产品在多端UI上的整体风格往往都是趋近统一的,采用原生各自独立完成功能开发后,还有多端差异的适配工作量。
- 性能体验:这是选择跨端技术方案的核心指标,一般来说,跨端技术方案拥有多重优势,但在性能方面相比原生差些。
2、跨平台技术划分与演进
- Web技术:采用WebView技术绘制界面的Hybrid混合开发技术,主要是通过JS Bridge将系统部分能力暴露给JS调用,缺点是功能受限、性能体验较差,而且不适合交互复杂的场景。
- 原生渲染:使用JavaScript作为编程语言,通过中间层转化为原生控件来渲染UI界面,相比WebView性能体验要更好,但相比原生还是有些差距,而且处理平台差异性比较耗费人力。
- 自渲染技术:自带渲染引擎,尽可能减少不同平台间的差异性,而且拥有媲美原生的高性能体验,因此Flutter出现之后就有着很高的关注度。
3、Flutter的优缺点
- 优点
- 基于Dart语言编写代码,开发体验更接近客户端,一套代码适用于多个平台(Android、iOS、Web),以及高效的Hot Reload能快速辅助调试。
- 实现UI像素级的控制,Flutter渲染引擎依靠跨平台Skia图形库来实现,仅依赖系统图形绘制相关的接口,可最大程度保证不同平台的体验一致性。
- 渲染性能优于现有的各种跨平台框架,可媲美原生,Dart代码执行的效率比JS高,通过AOT编译成平台原生代码,渲染采用自渲染skia方案,既不需要JS Bridge桥接,也不需要ART虚拟机参与。
- 缺点
- 对于复杂的场景脱离不开原生,目前来说更多的是一种UI框架,所以进行Flutter开发需要具备原生的基础开发能力。
- 总体上生态还不够健全,另外就是还有包体积的问题。
4、Flutter 技术架构
Flutter的技术结构跟Android原生技术架构类似,分为四层,从上往下依次是Dart APP,Dart Framework,C++ Engine和Platform。
- Flutter Framework层:用Dart编写,封装整个Flutter架构的核心功能,包括Widget、动画、绘制和手势等功能,另外还有Material(Android)和Cupertino(iOS)的UI风格界面。
- Flutter Engine层:用C++编写,实现了Flutter的核心库,当需要绘制一帧内容时,引擎将负责对需要合成的场景进行栅格化。它提供了 Flutter API 的底层实现,包括2D图形渲染库Skia、文本布局、文件及网络 IO、辅助功能支持、插件架构和 Dart 运行环境及编译环境的工具链,另外还有事件通知和平台通信通道等。
5、Flutter项目目录
- android:Android 平台相关代码
- ios:iOS 平台相关代码
- lib:Flutter 相关代码,我们主要编写的代码就在这个文件夹
- test:用于存放测试代码
- pubspec.yaml:配置文件,一般存放一些第三方库的依赖,本地资源目录,依赖的SDK版本等
每一个 flutter 项目的 lib 目录里面都有一个main.dart文件, 这个文件就是 flutter 的入口文件,在main.dart里有个runApp 方法,这个是flutter 的入口方法。MyApp 是自定义的一个组件。
void main() {
runApp(MyApp());
}
6、Widget概述
Widget是Flutter应用程序的基石,在Flutter里可以说是一切皆Widget,Widget可以是一个按钮,一种字体或者一个布局属性等,常见的Widget子类为StatelessWidget和StatefulWidget。
- StatelessWidget:内部没有保存状态,UI界面创建后不会发生改变
- StatefulWidget:内部有保存状态,当状态发生改变,调用setState()方法会触发StatefulWidget的UI发生更新,对于自定义继承自StatefulWidget的子类,必须要重写createState()方法。
7、Flutter渲染原理
需要了解Flutter的渲染原理,那么首先需要知道Flutter的三棵树
- Widget Tree:负责创建Element,为Element描述需要的配置,决定Element是否需要更新。Flutter Framework通过差分算法比对Widget树前后的变化,决定Element的State是否改变。当重建Widget树后并未发生改变, 则Element不会触发重绘。
- Element Tree:Element表示Widget配置树的特定位置的一个实例,同时持有Widget和RenderObject,负责管理Widget配置和RenderObject渲染。Element状态由Flutter Framework管理, 开发人员只需更改Widget即可。
- RenderObject Tree:RenderObject表示渲染树的一个对象,负责真正的渲染工作,比如测量大小、布局位置、绘制等都由RenderObject来完成。
Dart的UI采用Widget来实现,最终转换为RenderObject,界面的渲染过程:
- 当应用创建和展示场景的时候,UI线程完成布局、绘制操作,生成Layer Tree,一个包含设备无关的渲染命令的轻量对象,然后将Layer Tree发送到GPU线程。
- GPU线程执行合成并光栅化后交给GPU来处理,GPU线程拿到Layer Tree之后,通过skia向GPU硬件绘制一帧的数据,GPU将帧信息保存到FrameBuffer里面,然后视频控制器会根据VSync信号从FrameBuffer取帧数据传递给显示器,从而显示出最终的画面。
8、Flutter Channel渠道
Flutter channels:
master
dev
beta
* stable
- Stable:稳定版。
- Beta:公测版,比较稳定,每隔一段时间选取的最好的Dev版本。
- Dev:经过google测试后的,最新版本,包含新功能。
- Master:最新的代码主分支。
9、Flutter的构建模式
Flutter 支持三种模式编译 app:debug、profile和release,使用哪种模式构建主要取决于处于哪个开发阶段
- debug构建模式:开发过程中,需要使用热重载功能、断点的扩展服务的时候
- profile构建模式:profile和release模式的编译和运行基本相同,但它的一些服务扩展是启用的,可以连接到调试工具中,一般用于分析性能的时候,比如分析渲染性能和内存等。
- release构建模式:三者中最大的优化以及最小的占用空间,调试是禁用的,一般发布应用的时候使用release构建。
10、Flutter中的路由
在 Flutter中通过Navigator组件管理路由导航。并提供了管理堆栈的方法,如:Navigator.push 和 Navigator.pop,Flutter 中提供了两种配置路由跳转的方式:
- 基本路由
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context){
// 跳转的路由页面
return BlackListWidget(params: params);
}
));
- 命名路由
return MaterialApp(
onGenerateRoute: onGenerateRoute,
theme: ThemeData(
scaffoldBackgroundColor: DwColor.lightF8Gray,
),
);
/// 全局"动态"路由表<br/>
/// 支持以下两种方式传参数:
/// * [settings].name 按标准URL查询参数形式传递参数,如:/your_route_name?param1=value1¶m2=value2
/// * [settings].arguments 按[Map]<String, dynamic>格式传递参数,如:{'param1':'value1', 'param2':'value2'}
final RouteFactory onGenerateRoute = (RouteSettings settings) {
LogUtil.i('onGenerateRoute, ${settings.name}, ${settings.arguments}');
Map<String, dynamic>? params;
if (settings.arguments == null) {
params = {};
} else {
var arguments = settings.arguments;
params = Map<String, dynamic>.from(arguments as Map);
}
late String finalRouteName;
parseUri(settings.name, (routeName, args) {
finalRouteName = routeName;
if (finalRouteName != settings.name) {
//如果从settings.name解释回来的路径变了,意味着settings.name有携带参数,则以解释回来的参数为准,以免路径与参数不匹配
params = args;
}
});
params ??= {};
LogUtil.i('onGenerateRoute rst: finalRouteName=$finalRouteName, params=$params');
//获取页面Widget的同时对 params补充页面事件id(部分页面分根据现有的params动态给定页面id)
Widget home = _genPageWidgetByRoute(finalRouteName, params!);
return MaterialPageRoute(
builder: (context) => home,
// 重新生成RouteSettings,一方面路由名有可能已经因去除了链接参数而发生了变化
settings: RouteSettings(name: finalRouteName, arguments: params),
);
};
///根据路径及参数信息返回对应的页面
Widget _genPageWidgetByRoute(String path, Map<String, dynamic> params) {
Widget home;
switch (path) {
case RouteName.preWarm:
home = PreWarmPage();
break;
case RouteName.voiceBollFloatSetting:
home = VoiceBollFloatSettingWidget(params: params);
break;
case RouteName.blackList:
home = BlackListWidget(params: params);
break;
default:
home = VoiceBollFloatSettingWidget(params: params);
break;
}
return home;
}
// 跳转的路由页面
Navigator.pushNamed(context, RouteName.blackList, arguments:params);
11、Flutter混合开发代码依赖配置
gradle.properties
#是否为Flutter开发人员
isFlutterDev=true
#Flutter路径
flutterModulePath=../gamevoice_flutter
flutter.hostAppProjectName = gamevoice_client
settings.gradle
if (isFlutterDev.toBoolean()) {
//依赖本地Flutter模块
setBinding(new Binding([gradle: this]))
evaluate(new File(flutterModulePath + '/.android/include_flutter.groovy'))
include ':flutter_project'
project(':flutter_project').projectDir = new File(flutterModulePath)
}
build.gradle
if (!isFlutterDev.toBoolean()) {
//关于Flutter模块aar依赖的配置
implementation "com.gamevoice.gameshared.flutter:flutter:1.0.0-SNAPSHOT"
} else {
//关于Flutter模块源码依赖的配置
implementation project(':flutter')
}
12、Flutter引擎启动,跟native衔接
比如在Android中,FlutterApplication和FlutterActivity的onCreate()方法是连接Native和Flutter的枢纽。
FlutterApplication的onCreate方法主要完成初始化配置、加载引擎libflutter.so、注册JNI方法
FlutterActivity的onCreate方法,通过调用FlutterJNI来初始化引擎Engine、Dart虚拟机等对象。再经过层层处理最终调用main.dart中main()方法,执行runApp(Widget app)来处理整个Dart业务代码。
/**
* 提前预热的FlutterEngine,用于加速原生跳转Flutter页面
*/
public void preWarmFlutterEngine() {
MLog.info(TAG, "开始预热FlutterEngine");
FlutterEngine preWarmedEngine = FlutterEngineCache.getInstance().get(FlutterBridgeActivity.ENGINE_CACHE_CAR_MODULE);
//不确定之前预热的状态,当触发预热程序时,把之前的销毁
if (preWarmedEngine != null) {
try {
preWarmedEngine.destroy();
} catch (Exception e) {
MLog.error(TAG, e.getMessage());
}
}
preWarmedEngine = new FlutterEngine(YYMobileApp.gContext);
// preWarmedEngine.getNavigationChannel().setInitialRoute(FlutterApiConstants.FLUTTER_ROUTE_PRE_WARM);
preWarmedEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
// Cache the FlutterEngine to be used by FlutterActivity.
FlutterEngineCache
.getInstance()
.put(FlutterBridgeActivity.ENGINE_CACHE_CAR_MODULE, preWarmedEngine);
initWithFlutterEngine(preWarmedEngine);
}
13、platform channel
Flutter需要使用Camera等功能时需要原生平台的支持,Flutter通过提供platform channel来实现与原生平台之间的通信,这个过程的消息与响应是异步执行,不会阻塞用户界面,Flutter引擎框架已帮我们完成桥接的通道,主要有两种
- Method Channel:可以简单理解为Flutter需要调用原生的某些功能。
- Event Channel:可以简单理解为原生模块的一些改变需要通知Flutter模块做出相应处理。
14、总结:
从2018年12月Google 宣布 Flutter 1.0正式发布到现在不到3年的时间里,Flutter发展还是蛮快的,目前来说Flutter肯定是不够尽善尽美的,优点和缺点也是较为明显的,但是Flutter的想象空间还是蛮大的,方案的上限也比较高,如果要选一个跨平台方案的话,Flutter我觉得会是首选。