扣丁書屋

鴻蒙系統起飛!Flutter 完全適配指南

鴻蒙系統 (HarmonyOS)是華為推出的一款分布式操作系統,那么如何在保證開發迭代效率的前提下,以相對低的成本將移動應用快速移植到鴻蒙平臺上呢?美團外賣 MTFlutter 團隊近期做了一次技術探索,成功地實現了 Flutter 對于鴻蒙系統的原生支持。

前言

鴻蒙系統 (HarmonyOS)是華為推出的一款面向未來、面向全場景的分布式操作系統。在傳統單設備系統能力的基礎上,鴻蒙提出了基于同一套系統能力、適配多種終端形態的分布式理念。自 2020 年 9 月 HarmonyOS 2.0 發布以來,華為加快了鴻蒙系統大規模落地的步伐,預計 2021 年底,鴻蒙系統會覆蓋包括手機、平板、智能穿戴、智慧屏、車機在內的數億臺終端設備。對移動應用而言,新的系統理念、新的交互形式,也意味著新的機遇。如果能夠利用好鴻蒙的開發生態及其特性能力,可以讓應用覆蓋更多的交互場景和設備類型,從而帶來新的增長點。

與面臨的機遇相比,適配鴻蒙系統帶來的挑戰同樣巨大。當前手機端,盡管鴻蒙系統仍然支持安卓 APK 安裝及運行,但長期來看,華為勢必會拋棄 AOSP,逐步發展出自己的生態,這意味著現有安卓應用在鴻蒙設備上將會逐漸變成“二等公民”。然而,如果在 iOS 及 Android 之外再重新開發和維護一套鴻蒙應用,在如今業界越來越注重開發迭代效率的環境下,所帶來的開發成本也是難以估量的。因此,通過打造一套合適的跨端框架,以相對低的成本移植應用到鴻蒙平臺,并利用好該系統的特性能力,就成為了一個非常重要的選項。

在現有的眾多跨端框架當中,Flutter 以其自渲染能力帶來的多端高度一致性,在新系統的適配上有著突出的優勢。雖然 Flutter 官方并沒有適配鴻蒙的計劃(https://github.com/flutter/flutter/issues/38437),但經過一段時間的探索和實踐,美團外賣 MTFlutter 團隊成功實現了 Flutter 對于鴻蒙系統的原生支持。

這里也要提前說明一下,因為鴻蒙系統目前還處于 Beta 版本,所以這套適配方案還沒有在實際業務中上線,屬于技術層面比較前期的探索。接下來本文會通過原理和部分實現細節的介紹,分享我們在移植和開發過程中的一些經驗。希望能對大家有所啟發或者幫助。

背景知識和基礎概念介紹

在適配開始之前,我們要明確好先做哪些事情。先來回顧一下 Flutter 的三層結構:

在 Flutter 的架構設計中,最上層為框架層,使用 Dart 語言開發,面向 Flutter 業務的開發者;

中間層為引擎層,使用 C/C++ 開發,實現了 Flutter 的渲染管線和 Dart 運行時等基礎能力;

最下層為嵌入層,負責與平臺相關的能力實現。顯然我們要做的是將嵌入層移植到鴻蒙上,確切地說,我們要通過鴻蒙原生提供的平臺能力,重新實現一遍 Flutter 嵌入層。

對于 Flutter 嵌入層的適配,Flutter 官方有一份不算詳細的指南(https://github.com/flutter/flutter/wiki/Custom-Flutter-Engine-Embedders),實際操作起來成本很高。由于鴻蒙的業務開發語言仍然可用 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

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(https://developer.harmonyos.com/cn/docs/documentation/doc-references/native__layer-0000001060033509)。根據文檔的說明,Native API 中的 NativeLayer(https://developer.harmonyos.com/cn/docs/documentation/doc-references/native__layer-0000001060033509#EN-US_TOPIC_0000001060033509__ga10f0496160a17e00453c6744fb98a3f6)對象剛好對應了 Java 側的 Surface 類,借助 GetNativeLayer (https://developer.harmonyos.com/cn/docs/documentation/doc-references/native__layer-0000001060033509#EN-US_TOPIC_0000001060033509__ga10f0496160a17e00453c6744fb98a3f6)方法,我們實現了兩者之間的轉化:

// 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 實現(https://source.android.google.cn/devices/graphics/arch-egl-opengl?hl=zh-cn)中,EGLSurface 可通過 EGL 庫的 eglCreateWindowSurface(https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreateWindowSurface.xhtml)方法從本地窗口對象 ANativeWindow 創建而來。對于鴻蒙而言,雖然我們沒有從公開文檔找到類似的說明,但是鴻蒙標準庫(https://developer.harmonyos.com/cn/docs/documentation/doc-references/library-0000001060513586)默認支持了 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 事件傳遞的整個流程如下圖所示:

Flutter 事件分發

iOS/Android 的原生容器通過觸摸事件的回調 API 接收到事件之后,會將其打包傳遞至引擎層,后者將事件傳發給 Flutter 框架層,并完成事件的消費、分發和邏輯處理。同樣,整個流程的大部分工作已經由 Flutter 統一,我們要做的僅僅是在原生容器上監聽用戶的輸入,并封裝成指定格式交給引擎層而已。

在鴻蒙系統上,我們可以借助平臺提供的多模輸入 API(https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ui-multimodal-overview-0000000000031876),實現多種類型事件的監聽:

flutterComponent.setTouchEventListener(touchEventListener); // 觸摸及鼠標事件
flutterComponent.setKeyEventListener(keyEventListener); // 鍵盤錄入事件
flutterComponent.setSpeechEventListener(speechEventListener); // 語音錄入事件

對于事件的封裝處理,可以復用 Android 已有的邏輯,只需要關注鴻蒙與 Android 在事件處理上的對應關系即可,比如觸摸事件的部分對應關系:

3. 其他必要的平臺能力

為了保證 Flutter 應用能夠正常運行,除了最基本的渲染和交互外,我們的嵌入層還要提供資源管理、事件循環、生命周期同步等平臺能力。對于這些能力 Flutter 大多都在嵌入層的公共部分有抽象類聲明,只需要使用鴻蒙 API 重新實現一遍即可。

比如資源管理,引擎提供了 AssetResolver(https://github.com/flutter/engine/blob/master/assets/asset_resolver.h)聲明,我們可以使用鴻蒙 Rawfile(https://developer.harmonyos.com/cn/docs/documentation/doc-references/rawfile-0000001061151248)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(https://github.com/flutter/engine/blob/master/fml/message_loop_impl.h)抽象類,我們可以使用鴻蒙 Native_EventHandler(https://developer.harmonyos.com/cn/docs/documentation/doc-references/native__eventhandler-0000001054795159)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(https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ability-page-concept-0000000000033573)提供了完整的生命周期回調(如下圖所示),我們只需要在對應的時機將狀態上報給引擎即可。

Page Ability Lifecycle

當以上這些能力都準備好之后,我們就可以成功把 Flutter 應用跑起來了。以下是通過 DevEco Studio(https://developer.harmonyos.com/cn/develop/deveco-studio)運行官方 Flutter Gallery(https://github.com/flutter/gallery)應用的截圖,截圖中 Flutter 引擎已經使用鴻蒙系統的平臺能力進行了重寫:

DevEco 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

https://mp.weixin.qq.com/s/yolB-wFtntX3dvrqJJ1RiQ

最多閱讀

如何有效定位Flutter內存問題? 1年以前  |  11676次閱讀
Flutter的手勢GestureDetector分析詳解 3年以前  |  7605次閱讀
Flutter插件詳解及其發布插件 2年以前  |  6293次閱讀
在Flutter中添加資源和圖片 3年以前  |  5144次閱讀
Flutter 狀態管理指南之 Provider 3年以前  |  4341次閱讀
發布Flutter開發的iOS程序 3年以前  |  4317次閱讀
Flutter for Web詳細介紹 3年以前  |  4190次閱讀
在Flutter中發起HTTP網絡請求 3年以前  |  3954次閱讀
使用Inspector檢查用戶界面 3年以前  |  3885次閱讀
Flutter Widget框架概述 3年以前  |  3261次閱讀
Flutter路由詳解 3年以前  |  3103次閱讀
JSON和序列化 3年以前  |  3026次閱讀
Flutter框架概覽 3年以前  |  2990次閱讀
推薦5個Flutter重磅開源項目! 1年以前  |  2951次閱讀
為Flutter應用程序添加交互 3年以前  |  2941次閱讀
處理文本輸入 3年以前  |  2844次閱讀
使用自定義字體 3年以前  |  2843次閱讀
編寫國際化Flutter App 3年以前  |  2831次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>