引言
“一次编写,到处运行”,这是 SUN 公司用来展示 Java 语言跨平台特性的 slogan,它意味着 Java 可以在任何平台上进行开发,编译后就可以在任何安装有 Java 虚拟机(JVM)的设备上运行。美中不足的是,Java 虚拟机在各种操作系统(Windows, Linux, macOS, ...)上有多种不同的实现,导致 Java 应用程序在不同的虚拟机和操作系统上执行时有微妙的差别,因此它可能需要在许多平台上进行充分地测试才能确保正确性和稳定性,这造就了一个坊间笑话:“一次编译,到处调试”(Write Once, Debug Everywhere)—— 摘自维基百科
最近这两年,在移动端各种跨平台的开发方案如雨后春笋般涌现,一方面是因为随着移动互联网的普及和快速发展,移动终端设备的软硬件、操作系统、开发工具链和技术社区等日趋成熟完善;另一方面,近几年传统 PC 端的技术、资源也逐步迁移到移动端上来,大家都想造轮子,然后“一统天下”。—— 摘自掘金
在移动技术领域随着移动互联网的发展,“跨平台”的技术也有了迅猛的发展
从早先的基于浏览器内核的hybridApp方式 PhoneGap, Cordova
RN , Weex方案
基于web体系的 Progressive Web App
依赖超级app的小程序方案
越来越火的Flutter
当然还有很多在这场技术竞赛中没落可能都很少有人知道的如:Xamarin ,Ionic ......
(图片来源:闲鱼分享)
从单纯的项目角度来说,这是一个全新的工程,所以我们决定基于Flutter工程来创建,但是对于整个移动端的技术积累来说,此次还要使用我们已经开发的 存储,Hybrid,网络,路由等库。那么从顶层设计上来说,我们的整体框架如下:
在整个app的架构中我们主要做的工作都在橙色部分,Flutter承载了UI跨平台的主要功能,这也是Flutter框架本身的能力, 但是我们会发现,无论是新工程,还有已经存在的工程,我们都不可避免的存在混合开发的视图以及功能。无论是一个全新的flutter工程,还是现有的工程想部分转换为Flutter,都有以下的几个必经之路要做。
在前期的工程化设计中,参考了我们现有的项目结构,目前我们的项目结构中 Android使用的是MVP结构,iOS 也在进行MVVM的改造。主要核心还是基于UI和逻辑层分离,但是还要有数据驱动UI视图变更的能力。而 Flutter 本身就是这样的一个设计。所以我们核心是设定好工程规范。我们制定的工程规范如下:
工程代码管理
Api目录
Channels目录
Bases , Modules目录
目录的可选性可选目录是根据不同的场景需求在 “类mvvm” 架构上的松约束。
2、资源代码管理
Image.asset("resources/common/bg_image_placeholder.png")
3、依赖库管理
一个场景
第一张图:是由Native侧实现的Hybrid界面, 在这个界面里有一个功能,就是点赞, 当点击点赞功能的时候,会判断是否登录,如果没有登录则跳转到登录页面
第二张图:而第二张图就是使用 Flutter 开发的纯 Flutter UI 的界面
第三张图:在跳转切换的这个过程中,混合栈的问题就暴露出来了。
我们观察到 第三张图的两个页面的位置,flutter 页面在转场的过程中,其实是被Native的视图栈压在上面的。
第四张图:就是我们解决了该问题后的效果。视图栈的顺序和转场动画都正常了。
实际上这个问题的背后影响是下面的这张图
解决这个问题,有两种方案,第一种是以官方为代表的,当另外一个native页面启动flutter页面时,重新的创建引擎,来解决,以ios为例,当native要切换回flutter页面时,只需要在使用FlutterViewController重新创建engine实例,显示flutter页面即可。
优点:操作非常的简单,页面隔离性很好
缺点:页面数据复用非常困难,因为engine的内存是无法共享的, 也正因为内存无法共享创建多个engine将会带来更大的内存消耗
第二种方案:以闲鱼为代表的,共享engine的解决方案
优点:最大的有点是engine的引擎共享,相比第一种方案,启动速度会更加的快速。因为engine的创建还是有些耗时的。并且各个页面之间可以共享engine存储的数据。
缺点:每个flutter page的都有一个Native的ViewController或者Activity容器去承载,对于纯flutter工程来说还是有比较大的工程调整的成本
第三种方案: 通过消息驱动,在Native侧进行试图栈间的切换, 转场则使用截图方式进行动画补偿。
我们通过统一跳转入口,将 native 与 flutter 相互跳转的入口统一,基于flutter 提供的channel特性做native与flutter的交互通信,利用Stack Manager来进行栈的调配从而解决混合栈的问题。而channel本身的设计是基于底层native侧类似jni的方式进行交互的,所以性能也不受影响。在基于本身我们在 Native侧也有 路由的设计,所以我们将原有的路由方案做了一下升级,兼容了flutter从而也统一了我们native,flutter相互之间的跳转, 在跳转入口中,我们利用open url的方式来进行入口的规范和行为统一,基于url的特性也具备了不错的兼容和扩展性。 后面会讲到。
优点:因为我们并没有重新启动engine 所以 通过native开启的flutter页面,还是在同一个engine的基础上,满足了第二种方案的性能和数据同步的要求, 又因为对纯flutter项目为主的工程来说几乎没有迁移成本。
缺点:不适合以Native为主的工程,目前在深层级的交互创建页面中还需要优化(还在优化中)。
在原生的Native侧,我们是存在一套路由框架的,而且该原生框架, android , ios 逻辑统一并且功能上,我们已经支持:
1、内外部app之间的跳转能力
2、具备 Open URL 标准
3、支持页面拦截功能
4、其他一些安全以及业务层的封装
但是flutter有自带的router框架,经过我们的调研和实际的使用有如下的一些注意点:
在Flutter开发的页面间实现跳转的话,一切都离不开Navigator,系统提供了Navigator管理界面跳转、传参、返回等操作,但是使用起来不太方便,比如下图所示:router的注册需添加到routers中,界面跳转的话需要引入目的页的类,获取当前context,在进行跳转,获取也是一样,都比较复杂。
总结下来系统的router在我们的项目中存在如下问题:
1、使用不友好,并且与页面的耦合性较强
2、不支持无context的路由跳转
3、我们现有的路由扩展机制无法在flutter侧进行扩展
4、路由协议无法遵循我们现有的路由协议
5、Native Router与Flutter Router要分开管理
基于以上的一些原因,我们决定封装一套既可以满足flutter使用的路由,又兼容目前我们原生路由协议。所以我们在原有的native基础上做了基于flutter的跨平台统一路由:DRouter 基本流程如下图:
DRouter 统一了 android , ios 在路由接口上的平台差异,并且兼容了Native路由的能力,在Native 与 Flutter 侧都是用统一的api 抹平了全平台差异,也使开发人员不用关心路由对应的页面到底是flutter 还是 hybrid 或是native 这些处理都有DRouter统一管理了。由于篇幅内容原因,针对于DRouter后续会发一份更详细的文章进行说明。
下面简单介绍一下
microtask queue
,另一个叫做“事件队列” event queue
。微任务队列的执行优先级高于事件队列。try
/catch
/finally
来捕获代码块异常,其实在Dart中也是,我们看下Flutter系统中的异常捕获:源码(framework.dart)
abstract class ComponentElement extends Element {
ComponentElement(Widget widget) : super(widget);
Element _child;
@override
void performRebuild() {
if (!kReleaseMode && debugProfileBuildsEnabled)
Timeline.startSync('${widget.runtimeType}', arguments: timelineWhitelistArguments);
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
Widget built;
try {
// 构建页面
built = build();
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
// 构建错误页面并上报
built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
} finally {
_dirty = false;
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
}
try {
_child = updateChild(_child, built, slot);
assert(_child != null);
} catch (e, stack) {
built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
_child = updateChild(null, built, slot);
}
if (!kReleaseMode && debugProfileBuildsEnabled)
Timeline.finishSync();
}
@protected
Widget build();
}
framework.dart
代码可以看到,Element 是 Flutter 的核心,所有的 Wighet 是通过 Element树来渲染的,其中performRebuild()
是 Wighet 是渲染的核心函数,在调用 Wighet 的 build()
控件时就使用try
/catch
/finally
来捕获控件构建。从上面可以看出 Flutter 系统已经处理了异常捕获,并不会引起程序退出。异常页面
built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
似乎构建了一个新的 Widght 给 built 对象绘制到页面,继续跟踪: 源码(framework.dart)
class ErrorWidget extends LeafRenderObjectWidget {
ErrorWidget(Object exception)
: message = _stringify(exception),
_flutterError = exception is FlutterError ? exception : null,
super(key: UniqueKey());
// 默认错误页面
static ErrorWidgetBuilder builder = (FlutterErrorDetails details) => ErrorWidget(details.exception);
...
}
static
的静态函数体,并且返回的是一个提示异常信息 Widget,既然如此,我们是否可以替换这个 Widget呢?答案是可以的,在你的 main.dart
文件中的 main()
方法里,代码如下:Future main() async {
runApp(App());
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails) {
// 替换错误提示页面
return ErrorPage();
};
}
main.dart
文件中的 main()
方法里,替换错误页面时进行采集,代码如下:Future main() async {
runApp(App());
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails) {
// 上报异常 --> flutterErrorDetails
reportError(flutterErrorDetails.toString());
// 替换错误提示页面
return ErrorPage();
};
}
方法二、 有的同学可能还有印象,前面在构建默认页面时是使用_debugReportException()
函数进行封装FlutterErrorDetails
对象,看源码: 源码(framework.dart)
// 默认上报
FlutterErrorDetails _debugReportException(
DiagnosticsNode context,
dynamic exception,
StackTrace stack, {
InformationCollector informationCollector,
}) {
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets library',
context: context,
informationCollector: informationCollector,
);
// 默认上报异常
FlutterError.reportError(details);
return details;
}
在封装源码过程中调用了 FlutterError.reportError()
, 再跟: 源码(assertions.dart)
class FlutterError extends Error with DiagnosticableTreeMixin implements AssertionError {
static void reportError(FlutterErrorDetails details) {
assert(details != null);
assert(details.exception != null);
if (onError != null)
onError(details);
}
static FlutterExceptionHandler onError = dumpErrorToConsole;
}
与错误页面一样,最终指向了onError
函数体,那么同理,我们也可以替换该上报函数,在main.dart
中main()
替换:
Future main() async {
runApp(App());
FlutterError.onError = (FlutterErrorDetails details) {
// 上报异常 --> flutterErrorDetails
reportError(flutterErrorDetails.toString());
};
}
方法三、 用 Dart 中的 runZoned()
的方法,指定一个代码执行的环境空间,在这里可以捕获所有产生的异常问题( Zone
具体请查阅官方资料,此处不再讲解)。在main.dart
中main()
替换:
Future main() async {
runZoned(() {
runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
reportError(obj.toString());
});
}
另外需要注意的点:flutter中所产生的错误信息,并不属于崩溃。所以在处理crash信息上,要以错误信息来处理。而不是crash, 但是我们可以通过异常信息的规范制定,来设定自己的crash规则。进而监控crash率。
闲鱼
:属于行业内通过Flutter打响了自己的技术品牌,更主要的是通过Dart语言的特性与阿里内部的Faas开发服务的结合,升级了整个移动端的开发流程,将研发体系做了更深的升级 他们命名为 “云端一体化研发模式”,这个模式是什么呢?简单来讲,就是通过平台技术,让开发具备全栈的能力。(图片来源:淘宝技术分享)