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 引擎內部,遇到問題的時候可以做這三件事:
- Flutter GitHub 倉庫提 issue,等待官方解決;
- 定製引擎,編譯 Flutter 引擎找到問題並解決;
- 曲線規避問題發生場景;
當然在業務迭代中通常優先選擇第三點曲線規避當前問題,然後給官方提 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 檢視。