手把手教你使用PlaformView

1. 用PlatformView來作什麼

用PlatformView作音視頻直播!Flutter在UI呈現上具備極強的能力,但Widget在視頻渲染方面仍是存在不少不足的。目前市面上使用Flutter作視頻直播的主流方案有Texture Widget和PlatformView,Google官方開源的視頻播放插件video_player plugin是基於Texture Widget的。開發同窗的Flutter環境是1.9.1+hotfix-7,通過實驗咱們最終選擇PlatformView的方案來作。android

緣由有一下幾點:git

1)通過實際的實驗數據對比,PlatfomView的實際性能指標比純Native仍是要遜色一點的,但相差不遠
2)結合咱們現有的媒體sdk的接口,使用PlatformView比較實際
3)兄弟團隊早在Flutter1.0版本上有使用PlatformView的經驗
4)應對直播間複雜的UI交互,PlatformView實際使用起來更友好。
複製代碼

2. PlatformView究竟是個啥

2.1 PlatformView是一個經過flutter plugin的形式來建立NativeView的技術。github

PlatformView在Dart中的類對應到iOS和Android平臺分別是UiKitView和AndroidView,AndroidView和 UiKitView的定義本質是一個StatefulWidget。 以UiKitView爲例,先從Framework層來看看PlatformView定義以及PlatformView的建立流程。緩存

class UiKitView extends StatefulWidget {
  const UiKitView({
    Key key,
    @required this.viewType,
    this.onPlatformViewCreated,
    this.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
    this.layoutDirection,
    this.creationParams,
    this.creationParamsCodec,
    this.gestureRecognizers,
  }) : assert(viewType != null),
       assert(hitTestBehavior != null),
       assert(creationParams == null || creationParamsCodec != null),
       super(key: key);

    ````
  @override
  State<UiKitView> createState() => _UiKitViewState();
}

class _UiKitViewState extends State<UiKitView> {
    ````
    @override
    Widget build(BuildContext context) {
    return _UiKitPlatformView(
      controller: _controller,
      hitTestBehavior: widget.hitTestBehavior,
      gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet,
    );
  }

  Future<void> _createNewUiKitView() async {
      //id 是個++i的操做,會傳到在Engine中做爲key存儲當前建立的View
    final int id = platformViewsRegistry.getNextPlatformViewId();

    final UiKitViewController controller = await PlatformViewsService.initUiKitView(
      id: id,
      viewType: widget.viewType,
      layoutDirection: _layoutDirection,
      creationParams: widget.creationParams,
      creationParamsCodec: widget.creationParamsCodec,
    );
    if (!mounted) {
      controller.dispose();
      return;
    }
    if (widget.onPlatformViewCreated != null) {
      widget.onPlatformViewCreated(id);
    }
    setState(() { _controller = controller; });
  }
}

複製代碼

須要說明下viewType是註冊pluign時,Dart側和Native側約定的是必需要傳的一個字符傳,用來註冊PlatformView的類型。繼續看PlatformViewsService中initUiKitView的實現:bash

class PlatformViewsService {
    ````
  static Future<UiKitViewController> initUiKitView({
    @required int id,
    @required String viewType,
    @required TextDirection layoutDirection,
    dynamic creationParams,
    MessageCodec<dynamic> creationParamsCodec,
  }) async {
    assert(id != null);
    assert(viewType != null);
    assert(layoutDirection != null);
    assert(creationParams == null || creationParamsCodec != null);

    final Map<String, dynamic> args = <String, dynamic>{
      'id': id,
      'viewType': viewType,
    };
    if (creationParams != null) {
      final ByteData paramsByteData = creationParamsCodec.encodeMessage(creationParams);
      args['params'] = Uint8List.view(
        paramsByteData.buffer,
        0,
        paramsByteData.lengthInBytes,
      );
    }
    
    //重點在這裏,SystemChannels中定義了一些Flutter 與Engine以前進行通訊的channel, 如lifeCycle

    await SystemChannels.platform_views.invokeMethod<void>('create', args);
    return UiKitViewController._(id, layoutDirection);
  }
}
複製代碼

不難看出initUiKitView是經過SystemChannels中的platform_views這個channel與Native進行通訊,發送create消息到Native層。SystemChannels中定義了的channel給Dart與Engine之間進行通訊,所以咱們須要編譯下Flutter的Engine。繼續往下看create在Engine中的實現,最終定位到FlutterPlatformViews類OnCreate這個方法,核心實現代碼以下markdown

void FlutterPlatformViewsController::OnCreate(FlutterMethodCall* call, FlutterResult& result) {
    ````
  NSDictionary<NSString*, id>* args = [call arguments];
  long viewId = [args[@"id"] longValue];
  std::string viewType([args[@"viewType"] UTF8String]);
    //先根據viewId去查緩存
  if (views_.count(viewId) != 0) {
    result([FlutterError errorWithCode:@"recreating_view"
                               message:@"trying to create an already created view"
                               details:[NSString stringWithFormat:@"view id: '%ld'", viewId]]);
  }
    //檢測viewType的合法性
  NSObject<FlutterPlatformViewFactory>* factory = factories_[viewType].get();
  if (factory == nil) {
    result([FlutterError errorWithCode:@"unregistered_view_type"
                               message:@"trying to create a view with an unregistered type"
                               details:[NSString stringWithFormat:@"unregistered view type: '%@'",
                                                                  args[@"viewType"]]]);
    return;
  }

  id params = nil;
  //參數編碼格式是StandardMethodCodec類型,這個在platform_views這個channel初始化時有定義
  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];
    }
  }
    //根據FlutterPlatformView中的實現建立View
  NSObject<FlutterPlatformView>* embedded_view = [factory createWithFrame:CGRectZero
                                                           viewIdentifier:viewId
                                                                arguments:params];
  views_[viewId] = fml::scoped_nsobject<NSObject<FlutterPlatformView>>([embedded_view retain]);

    //建立一個Touch交互的View做爲咱們定義的embedded_view.view的父View
  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);
}
複製代碼

到此PlatformView建立調用流程就結束了。PlatformView在Dart側以UiKitView和AndroidView的形式暴露給開發者來建立NativeView;當PlatformView銷燬時,UiKitView的dispose方法會經過platform_views來向Engine發送dispose消息,在FlutterPlatformViews.mm的OnDispose方法中被銷燬。async

2.2 使用PlatformView的套路ide

從Flutter Engine層關於OnCreate的實現來看,咱們須要建立一個Flutter Plugin項目並註冊該plugin,Native端咱們要實現FlutterPlatformViewFactory以及FlutterPlatformView這兩個協議,在Plugin中根據viewType來註冊ViewFactory.直接上代碼。oop

2.2.1 FlutterPlatformViewFactory的實現佈局

@implementation RenderViewFactory

- (nonnull NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args
{
    RenderView *rendererView = [[RenderView alloc] initWithFrame:frame viewIdentifier:viewId];
    return rendererView;
}

- (NSObject<FlutterMessageCodec> *)createArgsCodec
{
    return [FlutterStandardMessageCodec sharedInstance];
}
@end

複製代碼

2.2.2 FlutterPlatformView的實現

@implementation RenderView
- (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId
{
    if (self = [super init]) {
        self.mainView = [[UIView alloc] initWithFrame:frame];
    }
    return self;
}

- (UIView *)view
{
    return self.mainView;
}

@end
複製代碼

2.2.3 註冊viewType和ViewFactory

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar
{
    FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:kRenderChannel
                                                                binaryMessenger:[registrar messenger]];
	//註冊ViewFactory和viewType 
    ThunderRenderViewFactory *renderViewFactory = [[ThunderRenderViewFactory alloc] init];
    [registrar registerViewFactory:renderViewFactory withId:kRenderViewType];
}
複製代碼

2.2.4 Dart層建立關聯的類

class RenderViewPlugin {
  static const MethodChannel _channel = MethodChannel(kRenderChannel);
 
 //提供一個建立NativeView的方法
  static Widget createRenderView(Function(int viewId) created, {Key key}) {
    if (defaultTargetPlatform == TargetPlatform.iOS) {
      return UiKitView(
        key: key,
        viewType: kRenderViewType,
        onPlatformViewCreated: (viewId) {
          if (created != null) {
            created(viewId);
          }
        },
      );
    } else if (defaultTargetPlatform == TargetPlatform.android){
      return AndroidView(
        key: key,
        viewType: kRenderViewType,
        onPlatformViewCreated: (viewId) {
          if (created != null) {
            created(viewId);
          }
        },
      );
    }
    return null;
  }
複製代碼

3. 在實際業務中使用PlatformView

3.1 一個實際的需求

直播間多人連麥場景下,要求視頻流渲染的View佈局隨着人數動態變化,且主播點擊任一路視頻流能夠全屏。
複製代碼

3.2 實現核心的視頻功能

Dart層推流localWidget和拉流remoteWidget均爲UiKitView來,localWidget和remoteWidget對應的Native View咱們給到媒體sdk視頻流渲染,localWidget和remoteWidget採用Stack佈局方便後面全屏模式下的層級切換。核心代碼以下:

class _LiveViewStackState extends State<LiveViewStack> {
    ····
  List<Widget> views = [
    Positioned(
        width: 200,
        height: MediaQuery.of(context).size.height,
        child: RenderViewPlugin.createNativeView((viewId) {
            _localViewId = viewId;
        }),
    ),
    Positioned(
        width: MediaQuery.of(context).size.width,
        height: 300,
        child: RenderViewPlugin.createNativeView((viewId) {
            _remoteViewId = viewId;
        }),
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
     body: Stack(
        children: videoViews(),
      ),
      floatingActionButton: RaisedButton(
        child: Text("切換層級"),
        onPressed: () {
         if (_localViewId != null && _remoteViewId != null) {
           if (mounted) {
             setState(() {
              views.insert(1, views.removeAt(0));
             });
           }
         }
        },
      ),
    );
  }
複製代碼

爲了不setState致使的NativeView被屢次建立,設置給媒體SDK的view跟當前在頁面上的view不是同一個致使黑屏, 所以咱們將localWidget和remoteWidget給Cache住。

3.3 遇到的問題

以上代碼代碼測試視頻流連麥沒有問題了,試試全屏切換,localWidget和remoteWidget的層級和尺寸沒有正常更新。 點擊層級切換,咱們指望的效果:

image
image

而實際咱們看到的效果:

image

image

3.4 解決方案

以iOS爲例,前面有提到Engine在建立UiKitView時會給UikitView添加一個父試圖FlutterTouchInterceptingView用來處理點擊事件,若是要切換兩個View的層級能夠這麼來作。

[frontView.superview.superview insertSubview:frontView.superview aboveSubview:backView.superview];
複製代碼

起初咱們也是這麼作的。但發現UiKitView在渲染以前根據viewId建立一個FlutterOverlayView的實例,這個overlay會覆蓋在UiKitView上,FlutterTouchInterceptingView有着共同的父試圖,咱們經過insertSubview切換了UiKitView,那麼FlutterOverlayView也應該切換纔對。FlutterOverlayView這個類是Engine私有並無對外公開,雖然咱們能夠經過一些手段拿到FlutterOverlayView可是成本過高,再者這麼作不太「Flutter」!

在Flutter中若是更新了Widget樹,咱們須要調用setState去觸發Element和RenderObject樹的更新,從而達咱們指望的UI效果。

樹的更新規則:

1)找到Widget對應的element節點,設置element爲dirty,觸發drawframe, drawframe會調用element的  performRebuild()進行樹重建

2)widget.build() == null, deactive element.child,刪除子樹,流程結束

3)element.child.widget == NULL, mount 的新子樹,流程結束

4)element.child.widget == widget.build() 無需重建,不然進入流程5

5)Widget.canUpdate(element.child.widget, newWidget) == true,更新child的slot,element.child.update(newWidget)(若是child還有子節點,則遞歸上面的流程進行子樹更新),流程結束,不然轉6

6)Widget.canUpdate(element.child.widget, newWidget) != true(widget的classtype 或者 key 不相等),deactivew element.child,mount 新子樹
複製代碼

核心方法在於canUpdate(element.child.widget, newWidget)當咱們沒有給Widget任何key的時候,將會只比較這兩個Widget的runtimeType 。這裏兩個Widget的runtimeType均爲咱們註冊PlatformView的viewType,canUpdate 方法將會返回true,因而更新StatefulWidget的位置,這兩個Element將不會交換位置。可是原有 Element 只會從它持有的state實例中build新的widget。由於element沒變,它持有的state也沒變, 所以就出現了上面的UI異常。

給localWidget和remoteWidgte分別加一個UniqueKey以後,canUpdate方法將會比較兩個Widget的runtimeType 以及 key。並返回false。此時 RenderObjectElement 會用新 Widget的key在老Element列表裏面查找,找到匹配的則會更新Element的位置並更新對應RenderObject的位置,所以就能更新成功了。

class _LiveViewStackState extends State<LiveViewStack> {
   ····
  List<Widget> views = [
    Positioned(
        key: UniqueKey(),
        width: 200,
        height: MediaQuery.of(context).size.height,
        child: RenderViewPlugin.createNativeView((viewId) {
            _localViewId = viewId;
        }),
    ),

    Positioned(
        key: UniqueKey(),
        width: MediaQuery.of(context).size.width,
        height: 300,
        child: RenderViewPlugin.createNativeView((viewId) {
            _remoteViewId = viewId;
        }),
    ),
  ];
  ····
複製代碼

4.總結

PlatformView的使用仍是很簡單的,在解決PlatformView上的內存問題後,從咱們實際的性能測試數據來看,音視頻的渲染性能表現基本是貼近原生。組內大佬這篇文章 手把手教你解決PlatformView內存泄漏 將PlatformView的內存泄漏以及Engine層對PlatfomrView的layout 、paint剖析的已經很清晰了,我就不作過多的贅述,這裏對比下某應用進出直播間的內存測試數據。

修復前:

image

修復後:

image

後面有機會嘗試Texture Widget的話,會對比下PlatformView的實際性能。

祝你們玩的開心!

做者

二蛋

相關文章
相關標籤/搜索