淺析 Flutter 與 iOS 的視圖橋樑

PlatformView 提供了在 Flutter 的 Widget 層級中嵌入原生視圖(iOS/Android等), PlatformView 在用來描述 iOS 平臺是視圖用的是 UIKitView,Android 平臺的視圖是 AndoirdView,本文全部描述都是針對 iOS 平臺,按官方的描述該功能仍是在發佈預覽階段,而且是很是昂貴的操做;如下是官方 API 文檔原文註釋:html

Embedding UIViews is still in release preview, to enable the preview for an iOS app add a boolean field with the key 'io.flutter.embedded_views_preview' and the value set to 'YES' to the application's Info.plist file. A list of open issued with embedding UIViews is available on Github. Embedding iOS views is an expensive operation and should be avoided when a Flutter equivalent is possible.

場景

每一個技術點的出現必然有它的價值所在,因此即使 PlatfromView 目前存在一些問題,而且 Flutter 自己就是一個 UI 框架,一些業務場景下只能依賴於它完成,例如:地圖、原生廣告、WebView等等;因此 Flutter 開發者仍是得點亮 PlatformView 技能樹;java

問題

在 Flutter1.12 版本中遇到過在 PageView、ListView 等容器視圖中將 PlatformView 移動到屏幕外,而且 Widget 沒銷燬的場景會引發引擎崩潰,因爲問題出在 Flutter 引擎內部,遇到問題的時候能夠作這三件事:ios

  1. Flutter GitHub 倉庫提 issue,等待官方解決;
  2. 定製引擎,編譯 Flutter 引擎找到問題並解決;
  3. 曲線規避問題發生場景;

固然在業務迭代中一般優先選擇第三點曲線規避當前問題,而後給官方提 issue,定製引擎這個選項最好在有足夠把握的時候選擇,不嚴謹的改動可能會引發一系列問題;git

使用流程

需求:建立一個能夠將黃色的 UIView 顯示到窗口的插件;

1. 建立 Flutter 插件

建立插件能夠經過命令行生成插件模板工程, 工程名只能用小寫:github

flutter create --template=plugin -i objc -a java platform_view

這裏建立的是 iOS 端使用 OC 語言 Android 端使用 Java 語言的插件,建立成功後能夠看到這樣的目錄結構:shell

2.封裝 UIKitView

在 lib 目錄下建立 color_view.dart 存放 UIKitView的一些操做,Flutter 能夠利用平臺通道 MethodChannel 與原平生臺進行數據交互,方法調用在發送以前被編碼爲二進制,接收到的二進制結果被解碼爲Dart值。api

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

const String singleColor = "singleColor";

class ColorView extends StatefulWidget {
  @override
  _ColorViewState createState() => _ColorViewState();
}

class _ColorViewState extends State<ColorView> {
  /// 平臺通道,消息使用平臺通道在客戶端(UI)和宿主(平臺)之間傳遞
  MethodChannel _channel;

  @override
  Widget build(BuildContext context) {
    return UiKitView(
      // 視圖類型,做爲惟一標識符
      viewType: singleColor,
      // 建立參數:將會傳遞給 iOS 端側, 能夠傳遞任意類型參數
      creationParams: "yellow",
      // 用於將creationParams編碼後再發送到平臺端。
      // 這裏使用Flutter標準二進制編碼
      creationParamsCodec: StandardMessageCodec(),
      // 原生視圖建立回調
      onPlatformViewCreated: _onPlatformViewCreated,
    );
  }

  /// 原生視圖建立回調操做
  /// id 是原生視圖惟一標識符
  void _onPlatformViewCreated(int id) {
    // 每一個 id 對應建立惟一的平臺通道
    _channel = MethodChannel('singleColor_$id');
    // 設置平臺通道的響應函數
    _channel.setMethodCallHandler(_handleMethod);
  }

  /// 平臺通道的響應函數
  Future<void> _handleMethod(MethodCall call) async {
    /// 視圖沒被裝載的狀況不響應操做
    if (!mounted) {
      return Future.value();
    }
    switch (call.method) {
      default:
        throw UnsupportedError("Unrecognized method");
    }
  }
}

3.添加 iOS 平臺代碼

使用 Xcode 編輯 iOS 平臺代碼以前,首先確保代碼至少被構建過一次,即從 IDE/編輯器執行示例程序,或在終端中執行如下命令:app

cd platform_view/example; flutter build ios --debug --no-codesign

打開 Platform_view/example/ios/Runner.xcworkspace iOS 工程,插件的 iOS 平臺代碼位於項目導航中的這個位置:框架

Pods/Development Pods/platform_view/../../example/ios/.symlinks/plugins/platform_view/ios/Classes

PlatformViewPlugin

此文件建立插件工程時生成的,在程序啓動的時候會將 AppDeleage 註冊進來, 這裏的 AppDeleage 繼承自 FlutterAppDelegate 遵照了 FlutterPluginRegistry, FlutterAppLifeCycleProvider 協議,前者爲了提供應用程序上下文和註冊回調的方法,後者爲了方便後續在插件中獲取應用生命週期事件;async

#import "PlatformViewPlugin.h"
#import "PlatfromViewFactory.h"

@implementation PlatformViewPlugin

/// 註冊插件
/// @param registrar 提供應用程序上下文和註冊回調的方法
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
    // 註冊視圖工廠
    // 綁定工廠惟一標識符這裏與 Flutter UIKitView 所使用 viewType 一致
    [registrar registerViewFactory:[[PlatfromViewFactory alloc] initWithMessenger:[registrar messenger]]
                            withId:@"singleColor"];
}

@end

PlatfromViewFactory

#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>

NS_ASSUME_NONNULL_BEGIN

@interface PlatfromViewFactory : NSObject<FlutterPlatformViewFactory>

/// 初始化視圖工廠
/// @param messager 用於與 Flutter 傳輸二進制消息通訊
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messager;

@end

NS_ASSUME_NONNULL_END
#import "PlatfromViewFactory.h"
#import "PlatformView.h"

@interface PlatfromViewFactory ()

/// 用於與 Flutter 傳輸二進制消息通訊
@property (nonatomic, strong) NSObject<FlutterBinaryMessenger> *messenger;

@end

@implementation PlatfromViewFactory

- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messager {
    self = [super init];
    if (self) {
        self.messenger = messager;
    }
    return self;
}

#pragma mark - FlutterPlatformViewFactory

/// 建立一個「FlutterPlatformView」
/// 由iOS代碼實現,該代碼公開了一個用於嵌入Flutter應用程序的「UIView」。
/// 這個方法的實現應該建立一個新的「UIView」並返回它。
/// @param frame Flutter經過其佈局widget來計算得來
/// @param viewId 視圖的惟一標識符,建立一個 UIKitView 該值會+1
/// @param args 對應Flutter 端UIKitView的creationParams參數
- (nonnull NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame
                                            viewIdentifier:(int64_t)viewId
                                                 arguments:(id _Nullable)args {
    PlatformView *platformView = [[PlatformView alloc] initWithWithFrame:frame
                                                          viewIdentifier:viewId
                                                               arguments:args
                                                         binaryMessenger:self.messenger];
    return platformView;
}

/// 使用Flutter標準二進制編碼
- (NSObject<FlutterMessageCodec> *)createArgsCodec {
    return [FlutterStandardMessageCodec sharedInstance];
}

@end

Flutter 端 UIKitView 的 viewType 與 工廠 ID 相同才能創建關聯,工廠的核心方法 createWithFrame,這裏三個參數都是由 Flutter 端傳遞過來的,UIKitView 的大小是由父 Widget 決定的,frame也就是 Flutter 經過其佈局 widget 來計算得來, viewId 是建立一個 UIKitView 該值會+1,而且是惟一的,args 對應 Flutter端 UIKitView 的 creationParams 參數;

PlatformView

PlatformView 繼承自 FlutterPlatformView 協議,工廠調用 PlatformView 對象來建立真正的 view 實例:

#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>

NS_ASSUME_NONNULL_BEGIN

@interface PlatformView : NSObject<FlutterPlatformView>

- (instancetype)initWithWithFrame:(CGRect)frame
                   viewIdentifier:(int64_t)viewId
                        arguments:(id _Nullable)args
                  binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;

@end

NS_ASSUME_NONNULL_END
#import "PlatformView.h"

@interface PlatformView ()

/// 視圖
@property (nonatomic, strong) UIView *yellowView;

/// 平臺通道
@property (nonatomic, strong) FlutterMethodChannel *channel;

@end

@implementation PlatformView

- (instancetype)initWithWithFrame:(CGRect)frame
                   viewIdentifier:(int64_t)viewId
                        arguments:(id _Nullable)args
                  binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {
    if ([super init]) {
        /// 初始化視圖
        self.yellowView = [[UIView alloc] init];
        self.yellowView.backgroundColor = UIColor.yellowColor;

        /// 這裏的channelName是和Flutter 建立MethodChannel時的名字保持一致的,保證一個原生視圖有一個平臺通道傳遞消息
        NSString *channelName = [NSString stringWithFormat:@"singleColor_%lld", viewId];
        self.channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger];
        // 處理 Flutter 發送的消息事件
        [self.channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) {
            if ([call.method isEqualToString:@""]) {

            }
        }];
    }
    return self;
}

#pragma mark - FlutterPlatformView
/// 返回真正的視圖
- (UIView *)view {
    return self.yellowView;
}

@end

4.使用

在 example工程中的 lib/main.dart 中使用封裝好的 ColorView:

import 'package:flutter/material.dart';
import 'package:platform_view/color_view.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('PlatformView Plugin'),
        ),
        body: Center(
          // 因爲原生視圖的大小由父 Widget 決定,
          // 這裏添加 Container 做爲父 Widget 並設置寬高爲 100
          child: Container(
            width: 100.0,
            height: 100.0,
            child: ColorView(),
          ),
        ),
      ),
    );
  }
}

5.開啓嵌入原生視圖功能

因爲嵌入 UIViews 仍在版本預覽中,默認此功能是關閉的,須要在 info.pilst 進行配置,開啓嵌入原生視圖:

<key>io.flutter.embedded_views_preview</key>
<true/>

6.運行結果

寬高各 100 的黃色 UIView 就顯示出來了,這裏只是舉了個最簡單的場景,能夠根據業務需求定製和原平生臺的交互。

源碼解析

1.原生視圖功能開關

剛剛咱們運行應用前在 info.plist 配置了開啓原生視圖預覽,能夠看到源碼中獲取了開啓狀態,在沒開啓的時候返回 nullptr ,嵌入式視圖要求 GPU 和平臺視圖的線程相同,即主線程;不開啓則是由 GPU 線程繪製畫布上的 UI;

// The name of the Info.plist flag to enable the embedded iOS views preview.
const char* const kEmbeddedViewsPreview = "io.flutter.embedded_views_preview";

bool IsIosEmbeddedViewsPreviewEnabled() {
  return [[[NSBundle mainBundle] objectForInfoDictionaryKey:@(kEmbeddedViewsPreview)] boolValue];
}
ExternalViewEmbedder* IOSSurfaceSoftware::GetExternalViewEmbedder() {
  if (IsIosEmbeddedViewsPreviewEnabled()) {
    return this;
  } else {
    return nullptr;
  }
}
if (flutter::IsIosEmbeddedViewsPreviewEnabled()) {
    // Embedded views requires the gpu and the platform views to be the same.
    // The plan is to eventually dynamically merge the threads when there's a
    // platform view in the layer tree.
    // For now we use a fixed thread configuration with the same thread used as the
    // gpu and platform task runner.
    // TODO(amirh/chinmaygarde): remove this, and dynamically change the thread configuration.
    // https://github.com/flutter/flutter/issues/23975

    flutter::TaskRunners task_runners(threadLabel.UTF8String,                          // label
                                      fml::MessageLoop::GetCurrent().GetTaskRunner(),  // platform
                                      fml::MessageLoop::GetCurrent().GetTaskRunner(),  // gpu
                                      _threadHost.ui_thread->GetTaskRunner(),          // ui
                                      _threadHost.io_thread->GetTaskRunner()           // io
    );
    // Create the shell. This is a blocking operation.
    _shell = flutter::Shell::Create(std::move(task_runners),  // task runners
                                    std::move(settings),      // settings
                                    on_create_platform_view,  // platform view creation
                                    on_create_rasterizer      // rasterzier creation
    );
  } else {
    flutter::TaskRunners task_runners(threadLabel.UTF8String,                          // label
                                      fml::MessageLoop::GetCurrent().GetTaskRunner(),  // platform
                                      _threadHost.gpu_thread->GetTaskRunner(),         // gpu
                                      _threadHost.ui_thread->GetTaskRunner(),          // ui
                                      _threadHost.io_thread->GetTaskRunner()           // io
    );
    // Create the shell. This is a blocking operation.
    _shell = flutter::Shell::Create(std::move(task_runners),  // task runners
                                    std::move(settings),      // settings
                                    on_create_platform_view,  // platform view creation
                                    on_create_rasterizer      // rasterzier creation
    );
  }

2.建立流程

接着來看看 UIKitView 建立後是怎麼到 iOS 端側的:

  • 點進 UIKitView 源碼能夠看到時一個 StafulWidget,接着看看它的 State 裏面實現;

getNextPlatformViewId實際上的操做是內部記錄了 viewId 的值,每次調用後+1;int getNextPlatformViewId() => _nextPlatformViewId++;後面的 UiKitViewController 看起來就是核心控制層了;

  • 能夠看到 Flutter 封裝了內部使用的 platform_views 平臺通道,發送了 create 事件;Flutter 的 framwork 層, 在原生視圖的事件響應中調用了 OnCreate 方法;

  • 最後咱們來看下 OnCreate 方法,代碼中截取了部分主要流程:
void FlutterPlatformViewsController::OnCreate(FlutterMethodCall* call, FlutterResult& result) {
  ...
  NSDictionary<NSString*, id>* args = [call arguments];
  // 獲取 viewid
  long viewId = [args[@"id"] longValue];
  // 獲取 viewType
  std::string viewType([args[@"viewType"] UTF8String]);
  ...
  // 經過 viewType 獲取視圖工廠
  NSObject<FlutterPlatformViewFactory>* factory = factories_[viewType].get();
  ...
  id params = nil;
  // 解碼參數
  if ([factory respondsToSelector:@selector(createArgsCodec)]) {
    NSObject<FlutterMessageCodec>* codec = [factory createArgsCodec];
    if (codec != nil && args[@"params"] != nil) {
      FlutterStandardTypedData* paramsData = args[@"params"];
      params = [codec decode:paramsData.data];
    }
  }
  // 經過視圖工廠建立嵌入視圖
  NSObject<FlutterPlatformView>* embedded_view = [factory createWithFrame:CGRectZero
                                                           viewIdentifier:viewId
                                                                arguments:params];
  views_[viewId] = fml::scoped_nsobject<NSObject<FlutterPlatformView>>([embedded_view retain]);

  // 將嵌入視圖添加到FlutterTouchInterceptingView中,
  // FlutterTouchInterceptingView主要負責處理手勢轉發和拒絕部分手勢,
  FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc]
       initWithEmbeddedView:embedded_view.view
      flutterViewController:flutter_view_controller_.get()] autorelease];

  // 存儲視圖
  touch_interceptors_[viewId] =
      fml::scoped_nsobject<FlutterTouchInterceptingView>([touch_interceptor retain]);
  root_views_[viewId] = fml::scoped_nsobject<UIView>([touch_interceptor retain]);

  result(nil);
}

3. 視圖分析

在建立視圖流程中引擎還默認添加了 FlutterOverlayView,目的是防止原生視圖遮擋 Flutter 視圖,原生視圖層級之上 Flutter 視圖都會繪製在 FlutterOverlayView 上,同一層級的視圖仍是繪製在 FlutterView 上面,這裏 FlutterView 和 FlutterOverlayView 都是 CAEAGLLayer,用於渲染 Flutter 視圖。

參考連接

  1. Flutter Packages 的開發和提交
  2. 撰寫雙端平臺代碼(插件編寫實現)
  3. UiKitView api 文檔
  4. Github Flutter Engine
相關文章
相關標籤/搜索