- 《架构师》2021年9月
- InfoQ中文站
- 3663字
- 2021-11-22 14:35:45
让Flutter在鸿蒙系统上跑起来
前言
鸿蒙系统(Harmony OS)是华为推出的一款面向未来、面向全场景的分布式操作系统。在传统单设备系统能力的基础上,鸿蒙提出了基于同一套系统能力、适配多种终端形态的分布式理念。自2020年9月Harmony OS 2.0发布以来,华为加快了鸿蒙系统大规模落地的步伐,预计2021年底,鸿蒙系统会覆盖包括手机、平板、智能穿戴、智慧屏、车机在内的数亿台终端设备。对移动应用而言,新的系统理念、新的交互形式,也意味着新的机遇。如果能够利用好鸿蒙的开发生态及其特性能力,可以让应用覆盖更多的交互场景和设备类型,从而带来新的增长点。
与面临的机遇相比,适配鸿蒙系统带来的挑战同样巨大。当前手机端,尽管鸿蒙系统仍然支持安卓APK安装及运行,但长期来看,华为势必会抛弃AOSP,逐步发展出自己的生态,这意味着现有安卓应用在鸿蒙设备上将会逐渐变成“二等公民”。然而,如果在i OS及Android之外再重新开发和维护一套鸿蒙应用,在如今业界越来越注重开发迭代效率的环境下,所带来的开发成本也是难以估量的。因此,通过打造一套合适的跨端框架,以相对低的成本移植应用到鸿蒙平台,并利用好该系统的特性能力,就成为了一个非常重要的选项。
在现有的众多跨端框架当中,Flutter以其自渲染能力带来的多端高度一致性,在新系统的适配上有着突出的优势。虽然Flutter官方并没有适配鸿蒙的计划,但经过一段时间的探索和实践,美团外卖MTFlutter团队成功实现了Flutter对于鸿蒙系统的原生支持。
这里也要提前说明一下,因为鸿蒙系统目前还处于Beta版本,所以这套适配方案还没有在实际业务中上线,属于技术层面比较前期的探索。接下来本文会通过原理和部分实现细节的介绍,分享我们在移植和开发过程中的一些经验。希望能对大家有所启发或者帮助。
背景知识和基础概念介绍
在适配开始之前,我们要明确好先做哪些事情。先来回顾一下Flutter的三层结构:
在Flutter的架构设计中,最上层为框架层,使用Dart语言开发,面向Flutter业务的开发者;中间层为引擎层,使用C/C++开发,实现了Flutter的渲染管线和Dart运行时等基础能力;最下层为嵌入层,负责与平台相关的能力实现。显然我们要做的是将嵌入层移植到鸿蒙上,确切地说,我们要通过鸿蒙原生提供的平台能力,重新实现一遍Flutter嵌入层。
对于Flutter嵌入层的适配,Flutter官方有一份不算详细的指南,实际操作起来成本很高。由于鸿蒙的业务开发语言仍然可用Java,在很多基础概念上与Android也有相似之处(如下表所示),我们可以从Android的实现入手,完成对鸿蒙的移植。
Flutter在鸿蒙上的适配
如前文所述,要完成Flutter在新系统上的移植,我们需要完整实现Flutter嵌入层要求的所有子模块,而从能力支持角度,渲染、交互以及其他必要的原生平台能力是保证Flutter应用能够运行起来的最基本的要素,需要优先支持。接下来会依次进行介绍。
1.渲染流程打通
我们再来回顾一下Flutter的图像渲染流程。如图所示,设备发起垂直同步(VSync)信号之后,先经过UI线程的渲染管线(Animate/Build/Layout/Paint),再经过Raster线程的组合和栅格化,最终通过Open GL或Vulkan将图像上屏。这个流程的大部分工作都由框架层和引擎层完成,对于鸿蒙的适配,我们主要关注的是与设备自身能力相关的问题,即:
(1)如何监听设备的VSync信号并通知Flutter引擎?(2)Open GL/Vulkan用于上屏的窗口对象从何而来?
VSync信号的监听及传递
在Flutter引擎的Android实现中,设备的VSync信号通过Choreographer触发,其产生及消费流程如下图所示:
Flutter VSync
Flutter框架注册VSync回调之后,通过C++侧的Vsync Waiter类等待VSync信号,后者通过JNI等一系列调用,最终Java侧的Vsync Waiter类调用Android SDK的Choreogra-pher.post Frame Callback方法,再通过JNI一层层传回Flutter引擎消费掉此回调。Java侧的Vsync Waiter核心代码如下:
@Override public void async Wait For Vsync(long cookie) { Choreographer.get Instance() .post Frame Callback( new Choreographer.Frame Callback() { @Override public void do Frame(long frame Time Nanos) { float fps = window Manager.get Default Display().get Refresh Rate(); long refresh Period Nanos = (long) (1000000000.0 / fps); Flutter JNI.native On Vsync( frame Time Nanos, frame Time Nanos + refresh Period Nanos, cookie); } }); }
在整个流程中,除了来自Android SDK的Choreographer以外,大多数逻辑几乎都由C++和Java的基础SDK实现,可以直接在鸿蒙上复用,问题是鸿蒙目前的API文档中尚没有开放类似Choreographer的能力。所以现阶段我们可以借用鸿蒙提供的类似i OS Grand Cen-tral Dispatch的线程API,模拟出VSync的信号触发与回调:
@Override public void async Wait For Vsync(long cookie) { // 模拟每秒 60 帧的屏幕刷新间隔:向主线程发送一个异步任务, 16ms后调用 application Context.get UITask Dispatcher().delay Dispatch(() -> { float fps = 60; // 设备刷新帧率,Harmony OS未暴露获取帧率API,先写死 60 帧 long refresh Period Nanos = (long) (1000000000.0 / fps); long frame Time Nanos = System.nano Time(); Flutter JNI.native On Vsync(frame Time Nanos, frame Time Nanos + refresh Period Nanos, cookie); }, 16); };
渲染窗口的构建及传递
在这一部分,我们需要在鸿蒙系统上构建平台容器,为Flutter引擎的图形渲染提供用于上屏的窗口对象。同样,我们参考Flutter for Android的实现,看一下Android系统是怎么做的:
Flutter在Android上支持Vulkan和Open GL两种渲染引擎,篇幅原因我们只关注Open GL。抛开复杂的注册及调用细节,本质上整个流程主要做了三件事:
1.创建了一个视图对象,提供可用于直接绘制的Surface,将它通过JNI传递给原生侧;
2.在原生侧获取Surface关联的本地窗口对象,并交给Flutter的平台容器;
3.将本地窗口对象转换为Open GL ES可识别的绘图表面(EGLSurface),用于Flutter引擎的渲染上屏。
接下来我们用鸿蒙提供的平台能力来实现这三点。
a.可用于直接绘制的视图对象
鸿蒙系统的UI框架提供了很多常用视图组件(Component),比如按钮、文字、图片、列表等,但我们需要抛开这些上层组件,获得直接绘制的能力。借助官方媒体播放器开发指导文档,可以发现鸿蒙提供了Surface Provider类,它管理的Surface对象可以用于视频解码后的展示。而Flutter渲染与视频上屏从原理上是类似的,因此我们可以借用Surface Provider实现Surface的管理和创建:
// 创建一个用于管理Surface的容器组件 Surface Provider surface Provider = new Surface Provider(context); // 注册视图创建回调 surface Provider.get Surface Ops().get().add Callback(surface Callback); // ...在surface Callback中 @Override public void surface Created(Surface Ops surface Ops) { Surface surface = surface Ops.get Surface(); // ...将surface通过JNI交给Native侧 Flutter JNI.on Surface Created(surface); }
b.与Surface关联的本地窗口对象
鸿蒙目前开放的Native API并不多,在官方文档中,我们可以比较容易地找到Native_layer API。根据文档的说明,Native API中的NativeLayer对象刚好对应了Java侧的Sur-face类,借助Get Native Layer方法,我们实现了两者之间的转化:
// platform_view_android_jni_impl.cc static void Surface Created(JNIEnv* env, jobject jcaller, jlong shell_holde r, jobject jsurface) { fml::jni::Scoped Java Local Frame scoped_local_reference_frame(env); // 通过鸿蒙Native API获取本地窗口对象Native Layer auto window = fml::Make Ref Counted<Android Native Window>( Get Native Layer(env, jsurface)); ANDROID_SHELL_HOLDER->Get Platform View()->Notify Created(std::move(windo w)); }
c.与本地窗口对象关联的EGLSurface
在Android的AOSP实现中,EGLSurface可通过EGL库的eglCreateWindowSurface方法从本地窗口对象ANative Window创建而来。对于鸿蒙而言,虽然我们没有从公开文档找到类似的说明,但是鸿蒙标准库默认支持了Open GL ES,而且鸿蒙SDK中也附带了EGL相关的库及头文件,我们有理由相信在鸿蒙系统上,EGLSurface也可以通过此方法从前一步生成的Native Layer转化而来,在之后的验证中我们也确认了这一点:
// window->handle() 即为之前得到的Native Layer EGLSurface surface = egl Create Window Surface( display, config_, reinterpret_cast<EGLNative Window Type>(window->handl e()), attribs); //...交给Flutter渲染管线
2.交互能力实现
交互能力是支撑Flutter应用能够正常运行的另一个基本要求。在Flutter中,交互包含了各种触摸事件、鼠标事件、键盘录入事件的传递及消费。以触摸事件为例,Flutter事件传递的整个流程如下图所示:
Flutter事件分发
i OS/Android的原生容器通过触摸事件的回调API接收到事件之后,会将其打包传递至引擎层,后者将事件传发给Flutter框架层,并完成事件的消费、分发和逻辑处理。同样,整个流程的大部分工作已经由Flutter统一,我们要做的仅仅是在原生容器上监听用户的输入,并封装成指定格式交给引擎层而已。
在鸿蒙系统上,我们可以借助平台提供的多模输入API,实现多种类型事件的监听:
flutter Component.set Touch Event Listener(touch Event Listener); // 触摸及鼠标事件 flutter Component.set Key Event Listener(key Event Listener); // 键盘录入事件 flutter Component.set Speech Event Listener(speech Event Listener); // 语音录入事 件
对于事件的封装处理,可以复用Android已有的逻辑,只需要关注鸿蒙与Android在事件处理上的对应关系即可,比如触摸事件的部分对应关系:
3.其他必要的平台能力
为了保证Flutter应用能够正常运行,除了最基本的渲染和交互外,我们的嵌入层还要提供资源管理、事件循环、生命周期同步等平台能力。对于这些能力Flutter大多都在嵌入层的公共部分有抽象类声明,只需要使用鸿蒙API重新实现一遍即可。
比如资源管理,引擎提供了AssetResolver声明,我们可以使用鸿蒙RawfileAPI来实现:
class HAPAsset Mapping : public fml::Mapping { public: HAPAsset Mapping(Raw File* asset) : asset_(asset) {} ~HAPAsset Mapping() override { Close Raw File(asset_); } size_t Get Size() const override { return Get Raw File Size(asset_); } const uint8_t* Get Mapping() const override { return reinterpret_cast<const uint8_t*>(Get Raw File Buffer(asset_)); } private: Raw File* const asset_; FML_DISALLOW_COPY_AND_ASSIGN(HAPAsset Mapping); };
对于事件循环,引擎提供了MessageLoopImpl抽象类,我们可以使用鸿蒙Native_EventHandlerAPI实现:
// runner_ 为鸿蒙Event Runner Native Implement的实例 void Message Loop Harmony::Run() { FML_DCHECK(runner_ == Get Event Runner Native Obj For Thread()); int result = ::Event Runner Run(runner_);
FML_DCHECK(result == 0); } void Message Loop Harmony::Terminate() { int result = ::Event Runner Stop(runner_); FML_DCHECK(result == 0); }
对于生命周期的同步,鸿蒙的Page Ability提供了完整的生命周期回调(如下图所示),我们只需要在对应的时机将状态上报给引擎即可。
Page Ability Lifecycle
当以上这些能力都准备好之后,我们就可以成功把Flutter应用跑起来了。以下是通过Dev Eco Studio运行官方Flutter Gallery应用的截图,截图中Flutter引擎已经使用鸿蒙系统的平台能力进行了重写:
Dev Eco Running Flutter
借由鸿蒙的多设备支持能力,此应用甚至可在TV、车机、手表、平板等设备上运行:
Flutter Multiple Devices
总结和展望
通过上述的构建和适配工作,我们以极小的开发成本实现了Flutter在鸿蒙系统上的移植,基于Flutter开发的上层业务几乎不做任何修改就可以在鸿蒙系统上原生运行,为迎接鸿蒙系统后续的大规模推广也提前做好了技术储备。
当然,故事到这里并没有结束。在最基本的运行和交互能力之上,我们更需要关注Flutter与鸿蒙自身生态的结合:如何优雅地适配鸿蒙的分布式技术?如何用Flutter实现设备之间的快速连接、资源共享?现有的众多Flutter插件如何应用到鸿蒙系统上?未来MTFlutter团队将在这些方面做更深入的探索,因为解决好这些问题,才是真正能让应用覆盖用户生活的全场景的关键。
参考文献:
https://developer.huawei.com/consumer/cn/events/hdc2020/
https://developer.harmonyos.com/cn/documentation
https://flutter.dev/docs/resources/architectural-overview
https://github.com/flutter/flutter/wiki/Custom-Flutter-Engine-Embedders
作者简介:
杨超,2016年加入美团外卖技术团队,目前主要负责MTFlutter相关的基础建设工作。