乐趣区

关于美团:让-Flutter-在鸿蒙系统上跑起来

前言

鸿蒙零碎 (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。抛开简单的注册及调用细节,实质上整个流程次要做了三件事:

  1. 创立了一个 视图对象,提供可用于间接绘制的 Surface,将它通过 JNI 传递给原生侧;
  2. 在原生侧获取 Surface 关联的 本地窗口对象,并交给 Flutter 的平台容器;
  3. 将本地窗口对象转换为 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 年货】、【算法】等关键词,可查看美团技术团队历年技术文章合集。

退出移动版