Flutter 1.20 下的 Hybrid Composition 深度解析

在以前的 《Android PlatformView 和鍵盤問題》 一文中介紹過混合開發(fā)上 Android PlatformView 的實現(xiàn)和問題尔觉,原本 Android 平臺上為了集成如 WebView塘雳、MapView等能力笨枯,使用了 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)方式最大的問題就在與觸摸事件填抬、文字輸入和鍵盤焦點等方面存在很多諸多需要處理的問題;在 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)的問題芥喇,比如華為手機上鍵盤彈出后 Web 界面離奇消失等玄學(xué)異常

使用 Hybrid Composition 需要使用到 PlatformViewLink凰萨、 AndroidViewSurfacePlatformViewsService 這三個對象继控,首先我們要創(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 的特點和實現(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)部主要有 ImageReader趾徽、ImageBitmap 三種類续滋,其中:

  • 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)在有 surface 谊囚、textureimage 三種 RenderMode

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

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

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

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

首先我們把上面第二小節(jié)的例子跑起來,同時打開 Android 手機的布局邊界猜拾,可以看到屏幕中間出現(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閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件波桩,死亡現(xiàn)場離奇詭異,居然都是意外死亡储玫,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進(jìn)店門匣椰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窝爪,“玉大人齐媒,你說我怎么就攤上這事⊙樱” “怎么了唬血?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵拷恨,是天一觀的道長。 經(jīng)常有香客問我小泉,道長冕杠,這世上最難降的妖魔是什么分预? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮配喳,結(jié)果婚禮上凳干,老公的妹妹穿的比我還像新娘纺座。我一直安慰自己溉潭,他們只是感情好少欺,可當(dāng)我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布赞别。 她就那樣靜靜地躺著配乓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪崎页。 梳的紋絲不亂的頭發(fā)上腰埂,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天屿笼,我揣著相機與錄音,去河邊找鬼休雌。 笑死肝断,一個胖子當(dāng)著我的面吹牛胸懈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播魁亦,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼洁奈,長吁一口氣:“原來是場噩夢啊……” “哼绞灼!你這毒婦竟也來了低矮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤昨悼,失蹤者是張志新(化名)和其女友劉穎跃洛,沒想到半個月后汇竭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡两曼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年玻驻,在試婚紗的時候發(fā)現(xiàn)自己被綠了击狮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖档冬,靈堂內(nèi)的尸體忽然破棺而出酷誓,到底是詐尸還是另有隱情,我是刑警寧澤棒拂,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布帚屉,位于F島的核電站攻旦,受9級特大地震影響生逸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜锋谐,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一皱炉、第九天 我趴在偏房一處隱蔽的房頂上張望狮鸭。 院中可真熱鬧合搅,春花似錦、人聲如沸歧蕉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽懊蒸。三九已至舌仍,卻和暖如春通危,著一層夾襖步出監(jiān)牢的瞬間菊碟,已是汗流浹背逆害。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工拧晕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留梅垄,地道東北人。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像赔嚎,于是被迫代替她去往敵國和親尤误。 傳聞我的和親對象是個殘疾皇子损晤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,884評論 2 354