用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.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.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的層級和尺寸沒有正常更新。 點擊層級切換,咱們指望的效果:
而實際咱們看到的效果:
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;
}),
),
];
····
複製代碼
PlatformView的使用仍是很簡單的,在解決PlatformView上的內存問題後,從咱們實際的性能測試數據來看,音視頻的渲染性能表現基本是貼近原生。組內大佬這篇文章 手把手教你解決PlatformView內存泄漏 將PlatformView的內存泄漏以及Engine層對PlatfomrView的layout 、paint剖析的已經很清晰了,我就不作過多的贅述,這裏對比下某應用進出直播間的內存測試數據。
修復前:
修復後:
後面有機會嘗試Texture Widget的話,會對比下PlatformView的實際性能。
祝你們玩的開心!