乐趣区

关于flutter:浅析-Flutter-与-iOS-的视图桥梁

PlatformView 提供了在 Flutter 的 Widget 层级中嵌入原生视图 (iOS/Android 等),PlatformView 在用来形容 iOS 平台是视图用的是 UIKitView,Android 平台的视图是 AndoirdView,本文所有形容都是针对 iOS 平台,按官网的形容该性能还是在公布预览阶段,并且是十分低廉的操作;以下是官网 API 文档原文正文:

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 技能树;

问题

在 Flutter1.12 版本中遇到过在 PageView、ListView 等容器视图中将 PlatformView 挪动到屏幕外,并且 Widget 没销毁的场景会引起引擎解体,因为问题出在 Flutter 引擎外部,遇到问题的时候能够做这三件事:

  1. Flutter GitHub 仓库提 issue,期待官网解决;
  2. 定制引擎,编译 Flutter 引擎找到问题并解决;
  3. 曲线躲避问题产生场景;

当然在业务迭代中通常优先选择第三点曲线躲避以后问题,而后给官网提 issue,定制引擎这个选项最好在有足够把握的时候抉择,不谨严的改变可能会引起一系列问题;

应用流程

需要:创立一个能够将黄色的 UIView 显示到窗口的插件;

1. 创立 Flutter 插件

创立插件能够通过命令行生成插件模板工程, 工程名只能用小写:

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

这里创立的是 iOS 端应用 OC 语言 Android 端应用 Java 语言的插件,创立胜利后能够看到这样的目录构造:

2. 封装 UIKitView

在 lib 目录下创立 color_view.dart 寄存 UIKitView 的一些操作,Flutter 能够利用平台通道 MethodChannel 与原生平台进行数据交互,办法调用在发送之前被编码为二进制,接管到的二进制后果被解码为 Dart 值。

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/ 编辑器执行示例程序,或在终端中执行以下命令:

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 协定,前者为了提供应用程序上下文和注册回调的办法,后者为了不便后续在插件中获取利用生命周期事件;

#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
退出移动版