前言
鸿蒙零碎 (HarmonyOS)是华为推出的一款面向未来、面向全场景的分布式操作系统。在传统单设施零碎能力的根底上,鸿蒙提出了基于同一套零碎能力、适配多种终端状态的分布式理念。自 2020 年 9 月 HarmonyOS 2.0 公布以来,华为放慢了鸿蒙零碎大规模落地的步调,预计 2021 年底,鸿蒙零碎会笼罩包含手机、平板、智能穿戴、智慧屏、车机在内数亿台终端设备。对挪动利用而言, 新的零碎理念、新的交互模式,也意味着新的时机。如果可能利用好鸿蒙的开发生态及其个性能力,能够让利用笼罩更多的交互场景和设施类型,从而带来新的增长点。
与面临的时机相比,适配鸿蒙零碎带来的挑战同样微小。以后手机端,只管鸿蒙零碎依然反对安卓 APK 装置及运行,但长期来看,华为势必会摈弃 AOSP,逐渐倒退出本人的生态,这意味着现有安卓利用在鸿蒙设施上将会逐步变成“二等公民”。然而,如果在 iOS 及 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 线程的组合和栅格化,最终通过 OpenGL 或 Vulkan 将图像 上屏。这个流程的大部分工作都由框架层和引擎层实现,对于鸿蒙的适配,咱们次要关注的是与设施本身能力相干的问题,即:
(1) 如何监听设施的 VSync 信号并告诉 Flutter 引擎?
(2) OpenGL/Vulkan 用于上屏的窗口对象从何而来?
VSync 信号的监听及传递
在 Flutter 引擎的 Android 实现中,设施的 VSync 信号通过 Choreographer 触发,它产生及生产流程如下图所示:
Flutter 框架注册 VSync 回调之后,通过 C++ 侧的 VsyncWaiter 类期待 VSync 信号,后者通过 JNI 等一系列调用,最终 Java 侧的 VsyncWaiter 类调用 Android SDK 的 Choreographer.postFrameCallback) 办法,再通过 JNI 一层层传回 Flutter 引擎生产掉此回调。Java 侧的 VsyncWaiter 外围代码如下:
@Override
public void asyncWaitForVsync(long cookie) {Choreographer.getInstance()
.postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {float fps = windowManager.getDefaultDisplay().getRefreshRate();
long refreshPeriodNanos = (long) (1000000000.0 / fps);
FlutterJNI.nativeOnVsync(frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
}
});
}
在整个流程中,除了来自 Android SDK 的 Choreographer 以外,大多数逻辑简直都由 C++ 和 Java 的根底 SDK 实现,能够间接在鸿蒙上复用,问题是鸿蒙目前的 API 文档中尚没有凋谢相似 Choreographer 的能力。所以现阶段咱们能够借用鸿蒙提供的相似 iOS Grand Central Dispatch 的线程 API,模拟出 VSync 的信号触发与回调:
@Override
public void asyncWaitForVsync(long cookie) {
// 模仿每秒 60 帧的屏幕刷新距离:向主线程发送一个异步工作, 16ms 后调用
applicationContext.getUITaskDispatcher().delayDispatch(() -> {
float fps = 60; // 设施刷新帧率,HarmonyOS 未裸露获取帧率 API,先写死 60 帧
long refreshPeriodNanos = (long) (1000000000.0 / fps);
long frameTimeNanos = System.nanoTime();
FlutterJNI.nativeOnVsync(frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
}, 16);
};
渲染窗口的构建及传递
在这一部分,咱们须要在鸿蒙零碎上构建平台容器,为 Flutter 引擎的图形渲染提供用于上屏的窗口对象。同样,咱们参考 Flutter for Android 的实现,看一下 Android 零碎是怎么做的:
Flutter 在 Android 上反对 Vulkan 和 OpenGL 两种渲染引擎,篇幅起因咱们只关注 OpenGL。抛开简单的注册及调用细节,实质上整个流程次要做了三件事:
- 创立了一个 视图对象,提供可用于间接绘制的 Surface,将它通过 JNI 传递给原生侧;
- 在原生侧获取 Surface 关联的 本地窗口对象,并交给 Flutter 的平台容器;
- 将本地窗口对象转换为 OpenGL ES 可辨认的 绘图外表(EGLSurface),用于 Flutter 引擎的渲染上屏。
接下来咱们用鸿蒙提供的平台能力实现这三点。
a. 可用于间接绘制的视图对象
鸿蒙零碎的 UI 框架提供了很多罕用视图组件(Component),比方按钮、文字、图片、列表等,但咱们须要抛开这些下层组件,取得间接绘制的能力。借助官网 媒体播放器开发领导 文档,能够发现鸿蒙提供了 SurfaceProvider 类,它治理的 Surface 对象能够用于视频解码后的展现。而 Flutter 渲染与视频上屏从原理上是相似的,因而咱们能够借用 SurfaceProvider 实现 Surface 的治理和创立:
// 创立一个用于治理 Surface 的容器组件
SurfaceProvider surfaceProvider = new SurfaceProvider(context);
// 注册视图创立回调
surfaceProvider.getSurfaceOps().get().addCallback(surfaceCallback);
// ... 在 surfaceCallback 中
@Override
public void surfaceCreated(SurfaceOps surfaceOps) {Surface surface = surfaceOps.getSurface();
// ... 将 surface 通过 JNI 交给 Native 侧
FlutterJNI.onSurfaceCreated(surface);
}
b. 与 Surface 关联的本地窗口对象
鸿蒙目前凋谢的 Native API 并不多,在官网文档中咱们能够比拟容易地找到 Native_layer API。依据文档的阐明,Native API 中的 NativeLayer 对象刚好对应了 Java 侧的 Surface 类,借助 GetNativeLayer 办法,咱们实现了两者之间的转化:
// platform_view_android_jni_impl.cc
static void SurfaceCreated(JNIEnv* env, jobject jcaller, jlong shell_holder, jobject jsurface) {fml::jni::ScopedJavaLocalFrame scoped_local_reference_frame(env);
// 通过鸿蒙 Native API 获取本地窗口对象 NativeLayer
auto window = fml::MakeRefCounted<AndroidNativeWindow>(GetNativeLayer(env, jsurface));
ANDROID_SHELL_HOLDER->GetPlatformView()->NotifyCreated(std::move(window));
}
c. 与本地窗口对象关联的 EGLSurface
在 Android 的 AOSP 实现中,EGLSurface 可通过 EGL 库的 eglCreateWindowSurface 办法从本地窗口对象 ANativeWindow 创立而来。对于鸿蒙而言,尽管咱们没有从公开文档找到相似的阐明,然而 鸿蒙规范库 默认反对了 OpenGL ES,而且鸿蒙 SDK 中也附带了 EGL 相干的库及头文件,咱们有理由置信在鸿蒙零碎上,EGLSurface 也能够通过此办法从前一步生成的 NativeLayer 转化而来,在之后的验证中咱们也确认了这一点:
// window->handle() 即为之前失去的 NativeLayer
EGLSurface surface = eglCreateWindowSurface(display, config_, reinterpret_cast<EGLNativeWindowType>(window->handle()),
attribs);
//... 交给 Flutter 渲染管线
2. 交互能力实现
交互能力 是撑持 Flutter 利用可能失常运行的另一个根本要求。在 Flutter 中,交互蕴含了各种触摸事件、鼠标事件、键盘录入事件的传递及生产。以触摸事件为例,Flutter 事件传递的整个流程如下图所示:
iOS/Android 的原生容器通过触摸事件的回调 API 接管到事件之后,会将其打包传递至引擎层,后者将事件传发给 Flutter 框架层,并实现事件的生产、散发和逻辑解决。同样,整个流程的大部分工作曾经由 Flutter 对立,咱们要做的仅仅是在原生容器上 监听 用户的输出,并 封装 成指定格局交给引擎层而已。
在鸿蒙零碎上,咱们能够借助平台提供的 多模输出 API,实现多种类型事件的监听:
flutterComponent.setTouchEventListener(touchEventListener); // 触摸及鼠标事件
flutterComponent.setKeyEventListener(keyEventListener); // 键盘录入事件
flutterComponent.setSpeechEventListener(speechEventListener); // 语音录入事件
对于事件的封装解决,能够复用 Android 已有逻辑,只须要关注鸿蒙与 Android 在事件处理上的对应关系即可,比方触摸事件的局部对应关系:
3. 其余必要的平台能力
为了保障 Flutter 利用可能失常运行,除了最根本的渲染和交互外,咱们的嵌入层还要提供资源管理、事件循环、生命周期同步等平台能力。对于这些能力 Flutter 大多都在嵌入层的公共局部有抽象类申明,只须要应用鸿蒙 API 从新实现一遍即可。
比方资源管理,引擎提供了 AssetResolver 申明,咱们能够应用鸿蒙 Rawfile API 来实现:
class HAPAssetMapping : public fml::Mapping {
public:
HAPAssetMapping(RawFile* asset) : asset_(asset) {}
~HAPAssetMapping() override { CloseRawFile(asset_); }
size_t GetSize() const override { return GetRawFileSize(asset_); }
const uint8_t* GetMapping() const override {return reinterpret_cast<const uint8_t*>(GetRawFileBuffer(asset_));
}
private:
RawFile* const asset_;
FML_DISALLOW_COPY_AND_ASSIGN(HAPAssetMapping);
};
对于事件循环,引擎提供了 MessageLoopImpl 抽象类,咱们能够应用鸿蒙 Native_EventHandler API 实现:
// runner_ 为鸿蒙 EventRunnerNativeImplement 的实例
void MessageLoopHarmony::Run() {FML_DCHECK(runner_ == GetEventRunnerNativeObjForThread());
int result = ::EventRunnerRun(runner_);
FML_DCHECK(result == 0);
}
void MessageLoopHarmony::Terminate() {int result = ::EventRunnerStop(runner_);
FML_DCHECK(result == 0);
}
对于生命周期的同步,鸿蒙的 Page Ability 提供了残缺的生命周期回调(如下图所示),咱们只须要在对应的机会将状态上报给引擎即可。
当以上这些能力都筹备好之后,咱们就能够胜利把 Flutter 利用跑起来了。以下是通过 DevEco Studio 运行官网 flutter gallery 利用的截图,截图中 Flutter 引擎曾经应用鸿蒙零碎的平台能力进行了重写:
借由鸿蒙的多设施反对能力,此利用甚至可在 TV、车机、手表、平板等设施上运行:
总结和瞻望
通过上述的构建和适配工作,咱们以极小的开发成本实现了 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 相干的根底建设工作。
| 想浏览更多技术文章,请关注美团技术团队(meituantech)官网微信公众号。
| 在公众号菜单栏回复【2019 年货】、【2018 年货】、【2017 年货】、【算法】等关键词,可查看美团技术团队历年技术文章合集。