Flutter 1.20 下的 Hybrid Composition 深度解析

在以前的 《Android PlatformView 和鍵盤問題》 一文中介紹過混合開發(fā)上 Android PlatformView 的實現(xiàn)和問題,原本 Android 平臺上為了集成如 WebViewMapView等能力,使用了 VirtualDisplays 的實現(xiàn)方式。

如今 1.20 官方開始嘗試推出和 iOS PlatformView 類似的新 Hybrid Composition 模式,本篇將通過三小節(jié)對比介紹 Hybrid Composition 的使用和原理,一起來吃“螃蟹”吧~

反復(fù)提醒,是 1.20 不是 1.2 ~~~

一、舊版本的 VirtualDisplay

1.20 之前在 Flutter 中通過將 AndroidView 需要渲染的內(nèi)容繪制到 VirtualDisplays
,然后在 VirtualDisplay 對應(yīng)的內(nèi)存中,繪制的畫面就可以通過其 Surface 獲取得到

VirtualDisplay 類似于一個虛擬顯示區(qū)域,需要結(jié)合 DisplayManager 一起調(diào)用,一般在副屏顯示或者錄屏場景下會用到。VirtualDisplay 會將虛擬顯示區(qū)域的內(nèi)容渲染在一個 Surface 上。

image

如上圖所示,簡單來說就是原生控件的內(nèi)容被繪制到內(nèi)存里,然后 Flutter Engine 通過相對應(yīng)的 textureId 就可以獲取到控件的渲染數(shù)據(jù)并顯示出來

這種實現(xiàn)方式最大的問題就在與觸摸事件、文字輸入和鍵盤焦點(diǎn)等方面存在很多諸多需要處理的問題;在 iOS 并不使用類似 VirtualDisplay 的方法,而是通過將 Flutter UI 分為兩個透明紋理來完成組合:一個在 iOS 平臺視圖之下,一個在其上面

所以這樣的好處就是:需要在“iOS平臺”視圖下方呈現(xiàn)的Flutter UI,最終會被繪制到其下方的紋理上;而需要在“平臺”上方呈現(xiàn)的Flutter UI,最終會被繪制在其上方的紋理。它們只需要在最后組合起來就可以了

通常這種方法更好,因為這意味著 Native View 可以直接參與到 Flutter 的 UI 層次結(jié)構(gòu)中。

二、 接入 Hybrid Composition

官方和社區(qū)不懈的努力下, 1.20 版本開始在 Android 上新增了 Hybrid CompositionPlatformView 實現(xiàn),該實現(xiàn)將解決以前存在于 Android 上的大部分和 PlatformView 相關(guān)的問題,比如華為手機(jī)上鍵盤彈出后 Web 界面離奇消失等玄學(xué)異常

使用 Hybrid Composition 需要使用到 PlatformViewLinkAndroidViewSurfacePlatformViewsService 這三個對象,首先我們要創(chuàng)建一個 dart 控件:

  • 通過 PlatformViewLinkviewType 注冊了一個和原生層對應(yīng)的注冊名稱,這和之前的 PlatformView 注冊一樣;
  • 然后在 surfaceFactory 返回一個 AndroidViewSurface 用于處理繪制和接受觸摸事件;
  • 最后在 onCreatePlatformView 方法使用 PlatformViewsService 初始化 AndroidViewSurface 和初始化所需要的參數(shù),同時通過 Engine 去觸發(fā)原生層的顯示。
Widget build(BuildContext context) {
  // This is used in the platform side to register the view.
  final String viewType = 'hybrid-view-type';
  // Pass parameters to the platform side.
  final Map<String, dynamic> creationParams = <String, dynamic>{};

  return PlatformViewLink(
    viewType: viewType, 
    surfaceFactory:
        (BuildContext context, PlatformViewController controller) {
      return AndroidViewSurface(
        controller: controller,
        gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
        hitTestBehavior: PlatformViewHitTestBehavior.opaque,
      );
    },
    onCreatePlatformView: (PlatformViewCreationParams params) {
      return PlatformViewsService.initSurfaceAndroidView(
        id: params.id,
        viewType: viewType,
        layoutDirection: TextDirection.ltr,
        creationParams: creationParams,
        creationParamsCodec: StandardMessageCodec(),
      )
        ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
        ..create();
    },
  );
}

接下來來到 Android 原生層,在原生通過繼承 PlatformView 然后通過 getView 方法返回需要渲染的控件。

package dev.flutter.example;

import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugin.platform.PlatformView;

class NativeView implements PlatformView {
   @NonNull private final TextView textView;

    NativeView(@NonNull Context context, int id, @Nullable Map<String, Object> creationParams) {
        textView = new TextView(context);
        textView.setTextSize(72);
        textView.setBackgroundColor(Color.rgb(255, 255, 255));
        textView.setText("Rendered on a native Android view (id: " + id + ")");
    }

    @NonNull
    @Override
    public View getView() {
        return textView;
    }

    @Override
    public void dispose() {}
}

之后再繼承 PlatformViewFactory 通過 create 方法來加載和初始化 PlatformView

package dev.flutter.example;

import android.content.Context;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
import java.util.Map;

class NativeViewFactory extends PlatformViewFactory {
  @NonNull private final BinaryMessenger messenger;
  @NonNull private final View containerView;

  NativeViewFactory(@NonNull BinaryMessenger messenger, @NonNull View containerView) {
    super(StandardMessageCodec.INSTANCE);
    this.messenger = messenger;
    this.containerView = containerView;
  }

  @NonNull
  @Override
  public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
    final Map<String, Object> creationParams = (Map<String, Object>) args;
    return new NativeView(context, id, creationParams);
  }
}

最后在 MainActivity 通過 flutterEnginegetPlatformViewsController 去注冊 NativeViewFactory

package dev.flutter.example;

import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;

public class MainActivity extends FlutterActivity {
    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        flutterEngine
            .getPlatformViewsController()
            .getRegistry()
            .registerViewFactory("hybrid-view-type", new NativeViewFactory(null, null));
    }
}

當(dāng)然,如果需要在 Android 上啟用 Hybrid Composition ,還需要在 AndroidManifest.xml 添加如下所示代碼來啟用配置:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="dev.flutter.example">
    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="hybrid"
        android:icon="@mipmap/ic_launcher">
        <!-- ... -->
        <!-- Hybrid composition -->
        <meta-data
            android:name="io.flutter.embedded_views_preview"
            android:value="true" />
    </application>
</manifest>

另外,官方表示 Hybrid composition 在 Android 10 以上的性能表現(xiàn)不錯,在 10 以下的版本中,F(xiàn)lutter 界面在屏幕上呈現(xiàn)的速度會變慢,這個開銷是因為 Flutter 幀需要與 Android 視圖系統(tǒng)同步造成的。

為了緩解此問題,應(yīng)該避免在 Dart 執(zhí)行動畫時顯示原生控件,例如可以使用placeholder 來原生控件的屏幕截圖,并在這些動畫發(fā)生時直接使用這個 placeholder

三、 Hybrid Composition 的特點(diǎn)和實現(xiàn)原理

要介紹 Hybrid Composition 的實現(xiàn),就不得不介紹本次新增的一個對象:FlutterImageView

FlutterImageView 并不是一般意義上的 ImageView

事實上 Hybrid Composition 上混合原生控件所需的圖層合成就是通過 FlutterImageView 來實現(xiàn)。FlutterImageView 本身是一個普通的原生 View, 它通過實現(xiàn)了 RenderSurface 接口從而實現(xiàn)如 FlutterSurfaceView 的部分能力。

FlutterImageView 內(nèi)部主要有 ImageReaderImageBitmap 三種類,其中:

  • ImageReader 可以簡單理解為就是能夠存儲 Image 數(shù)據(jù)的對象,并且可以提供 Surface 用于繪制接受原生層的 Image 數(shù)據(jù)。
  • Image 就是包含了 ByteBuffers 的像素數(shù)據(jù),它和 ImageReader 一般用在原生的如 Camera 相關(guān)的領(lǐng)域。
  • Bitmap 是將 Image 轉(zhuǎn)化為可以繪制的位圖,然后在 FlutterImageView 內(nèi)通過 Canvas 繪制出來。

可以看到 FlutterImageView 可以提供 Surface ,可以讀取到 SurfaceImage 數(shù)據(jù),然后通過Bitmap 繪制出來。

而在 FlutterImageView 中提供有 backgroundoverlay 兩種 SurfaceKind ,其中:

  • background 適用于默認(rèn)下 FlutterView 的渲染模式,也就是 Flutter 主應(yīng)用的渲染默認(rèn),所以 FlutterView 其實現(xiàn)在有 surfacetextureimage 三種 RenderMode

  • overlay 就是用于上面所說的 Hybrid Composition 下用于和 PlatformView 合成的模式。

另外還有一點(diǎn)可以看到,在 PlatformViewsController 里有 createAndroidViewForPlatformViewcreateVirtualDisplayForPlatformView 兩個方法,這也是 Flutter 官方在提供 Hybrid Composition 的同時也兼容 VirtualDisplay 默認(rèn)的一種做法。

Hybrid Composition Dart 層通過 PlatformViewsService 觸發(fā)原生的 PlatformViewsChannelcreate 方法,之后發(fā)起一個 PlatformViewCreationRequest 就會有 usesHybridComposition 的判斷,如果為 ture 后面就是走的 createAndroidViewForPlatformView

那么 Hybrid Composition 模式下 FlutterImageView 是如何工作的呢?

首先我們把上面第二小節(jié)的例子跑起來,同時打開 Android 手機(jī)的布局邊界,可以看到屏幕中間出現(xiàn)了一個包含 Re 的白色小方塊。通過布局邊界可以看到, Re 白色小方塊其實是一個原生控件。

image

接著用同樣的代碼在不同位置增加一個 Re 白色小方塊,可以看到屏幕的右上角又多了一個有布局邊界的 Re 白色小方塊,所以可以看到 Hybrid Composition 模式下的 PlatformView 是通過某種原生控件顯示出來的。

image

但是我們就會想了,在 Flutter 上放原生控件有什么稀奇的?這就算是圖層合成了?那么接著把兩個 Re 白色小方塊放到一起,然后在它們上面不用 PlatformView 而是直接用默認(rèn)的 Text 繪制一個藍(lán)色的 Re文本。

image

看到?jīng)]有?在不用 PlatformView 的情況下,Text 繪制的藍(lán)色的 Re文本居然可以顯示在白色不透明的原生 Re 白色小方塊上!!!

也許有的小伙伴會說,這有什么稀奇的?但是知道 Flutter 首先原理的應(yīng)該知道,Flutter 在原生層默認(rèn)情況下就是一個 SurfaceView,然后 Engine 把所有畫面控件渲染到這個 Surface 上。

但是現(xiàn)在你看到了什么?我們在 Dart 層的 Text 藍(lán)色的 Re 文本居然可以現(xiàn)在到 Re 白色小方塊上,這說明 Hybrid Composition 不僅僅是把原生控件放到 Flutter 上那么簡單。

然后我們又發(fā)現(xiàn)了另外一個奇怪的問題,用 Flutter 默認(rèn) Text 繪制的藍(lán)色的 Re 文本居然也有原生的布局邊界顯示?所以我們又用默認(rèn) Text 增加了黃色的 Re 文本和紅色的 Re 文本 ,可以看到只有和 PlatformView 有交集的 Text 出現(xiàn)了布局邊界。

image

接著將黃色的 Re 文本往下調(diào)整后,可以看到黃色 Re 文本的布局邊界也消失了,所以可以判定 Hybrid Composition 下 Dart 控件之所以可以顯示在原生控件之上,是因為在和 PlatformView 有交集時通過某種原生控件重新繪制。

image

所以我們通過 Layout Inspector 可以看到,重疊的 Text 控件是通過 FlutterImageView 層來實現(xiàn)渲染

image

另外還有一個有趣的現(xiàn)象,那就是當(dāng) Flutter 有不只一個默認(rèn)的控件本被顯示在一個 PlatformView 區(qū)域上時,那么這幾個控件會共用一個 FlutterImageView

image

而如果他們不在一個區(qū)域內(nèi),那么就會各自使用自己的 FlutterImageView 。另外可以注意到,Hybrid Composition 默認(rèn)接入的 PlatformView 是一個 FlutterMutatorView

image

其實 FlutterMutatorView 是用于調(diào)整原生控件接入到 FlutterView 的位置和 Matrix 的,一般情況下 Hybrid Composition 下的 PlatformView 接入關(guān)系是:

image

所以 PlatformView 是通過 FlutterMutatorView 把原生控件 addViewFlutterView 上,然后再通過 FlutterImageView 的能力去實現(xiàn)圖層的混合

那么 Flutter 是怎么判斷控件需要使用 FlutterImageView

事實上可以看到,在 Engine 去 SubmitFrame 時,會通過 current_frame_view_count 去對每個 view 畫面進(jìn)行規(guī)劃處理,然后會通過判定區(qū)域內(nèi)是否需要 CreateSurfaceIfNeeded 函數(shù),最終觸發(fā)原生的 createOverlaySurface 方法去創(chuàng)建 FlutterImageView

    for (const SkRect& overlay_rect : overlay_layers.at(view_id)) {
      std::unique_ptr<SurfaceFrame> frame =
          CreateSurfaceIfNeeded(context,               //
                                view_id,               //
                                pictures.at(view_id),  //
                                overlay_rect           //
          );
      if (should_submit_current_frame) {
        frame->Submit();
      }
    }

至于在 Dart 層面 PlatformViewSurface 就是通過 PlatformViewRenderBox 去添加 PlatformViewLayer ,然后再通過在 ui.SceneBuilderaddPlatformView 調(diào)用 Engine 添加 Layer 信息。(這部分內(nèi)容可見 《 Flutter 畫面渲染的全面解析》

其實還有很多的實現(xiàn)細(xì)節(jié)沒介紹,比如:

  • onDisplayPlatformView 方法,也就是在展示 PlatformView 時,會調(diào)用 flutterView.convertToImageView 方法將 renderSurface 切換為 flutterImageView
  • initializePlatformViewIfNeeded 方法里初始化過的 PlatformViews 不會再次初始化創(chuàng)建;
  • FlutterImagaeViewcreateImageReaderupdateCurrentBitmap 時, Android 10 上可以通過 GPU 實現(xiàn)硬件加速,這也是為什么 Hybrid Composition 在 Android 10 上性能較好的原因。

因為篇(tou)幅(lan)剩下就不一一展開了,目前 Hybrid Composition 已經(jīng)在 1.20 stable 版本上可用了,也解決了我在鍵盤上的一些問題,當(dāng)然 Hybrid Composition 能否經(jīng)受住考驗?zāi)侵荒茏寱r間決定了,畢竟一步一個坑不是么~

資源推薦

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,707評論 2 370