写点什么

Pano Flutter SDK 设计经验与实践浅谈

用户头像
拍乐云Pano
关注
发布于: 3 小时前
Pano Flutter SDK 设计经验与实践浅谈

Flutter 是谷歌推出的移动 UI 框架,可以快速在 iOS 和 Android 上构建高质量的原生用户界面,被越来越多的开发者选择和使用。拍乐云也提供了功能强大的 Pano Flutter SDK,性能稳定且易用,覆盖语音通话、视频通话、互动白板、互动直播、云端录制等各种功能。在之前的一篇《Pano Flutter SDK 全新发布》中,我们给大家介绍了 SDK 的详细接入流程,今天将继续聊聊我们 Pano Flutter SDK 的设计思路与实践经验。

#1

总体结构

Pano 针对原生应用开发提供了完备的高性能 SDK,所以 Pano Flutter SDK 采用插件包的形式来封装我们的 SDK。类似的,在 RN 中我们采用 NativeModule 来实现 Pano RN SDK。Pano 移动端跨平台 SDK 的总体结构如下图所示:



SDK 分为三层结构,底层为 Pano 原生 SDK(iOS&Android)。基于原生 SDK 之上为桥接层,由于 Flutter 与 RN 中与原生层通信均为异步通信,且需使用特定的通信方式(Flutter 使用平台通道方案,RN 则使用原生模块方案),所以需要将跨平台调用进行转换才能调用原生 SDK 方法。因此桥接层将分为两个部分,原生 SDK 桥接与跨平台(Flutter&RN)桥接,以达到最大化代码复用的目的,将原生 SDK 接口二次封装成通用异步接口,在其上分别对接 Flutter 和 RN 的通信接口。SDK 最顶层则为跨平台层,对接原生层通信接口封装出 Flutter 或 RN 平台的功能接口。

虽然最终的结构比较简洁明了,但是由于 Flutter 或 RN 对于视图更新机制与原生开发存在较大差异,以及跨平台层与原生层数据结构的不同等问题,导致 SDK 的设计与实现中存在许多涉及数据转换、对象映射、内存管理等难点或坑点,接下来将结合 SDK 的设计思路与实践经验,针对其中几个典型的问题谈谈解决方案或需要注意的地方。

#2

工作流程

Pano Flutter SDK 提供的 API 基本上与原生 SDK 保持一一对应的关系,以便开发人员可以轻松的将对接原生 SDK 开发经验应用到 Flutter 中。但由于 Flutter 特殊的平台通道(Platform Channel)方案以及视图更新机制,所以并不是简单的将原生 SDK 接口进行透传封装,SDK 的调用流程如下图所示:



SDK 使用 Flutter 平台通道中 MethodChannel 与 EventChannel 来实现 Flutter 层与原生层通信,其中 MethodChannel 用于 Flutter 向原生层方法调用,EventChannel 则用于原生层向 Flutter 层进行数据流通信,这里主要是传递原生层回调消息。当开发者调用 Flutter 层接口,SDK 使用对应的 MethodChannel 将方法名、参数传递到原生层,SDK 在这里实现了 Flutter Native Bridge 来专门处理这些调用。

建议:当原生层接收到 MethodChannel 的方法调用时(例如:iOS 为-[FlutterPlugin handleMethodCall:result:]),采用反射调用(例如:iOS 中使用NSSelectorFromString获取 selector,然后通过[NSObject performSelector:withObject:]调用)Native SDK Bridge 方法,这样可以尽量将 Flutter 的逻辑与原生桥接层逻辑隔离,一方面做薄对接 Flutter 层逻辑,另一方面将需要经常跟随原生 SDK 变动的原生桥接层逻辑与其它跨平台框架(如 RN)进行复用,减少维护成本。


注意:Flutter 中没有现成的二进制数据类型,通常采用Uint8List来代替,但通过平台通道转换后,在 iOS 端会转换成FlutterStandardTypedData类型,该类型不能自动转换为NSData类型,需要通过其属性data来获取实际的NSData对象。但在从原生层调用 Flutter 层时,可以直接传递NSData对象,其将会在 Flutter 层被自动转换为Uint8List

Flutter 中平台通道实际上是将传递的数据编码成消息的形式,跨线程发送到该应用所在的宿主原生层。并且 Native SDK Bridge 对接原生 SDK,将原生 SDK 方法实行完毕后的返回值通过 callback 返回时,也是将数据编码成消息通过同样方式原路返回给 Flutter 层。整个过程的消息和响应是异步的,这也就是 Flutter 层接口都设计成异步接口的原因。

注意:MethodChannel 类型中,调用原生方法使用Future<T?> invokeMethod<T>(String method, [ dynamic arguments ]),对于 SDK 返回 Flutter 支持的基本类型数据时,直接调用没有任何问题,例如当获取 SDK 版本号接口返回String类型,则 Flutter 层接口可以实现为:static Future<String> getSdkVersion() {


// iOS 中 NSString 和 Andorid 中 java.lang.String 都可以自动转换为 Flutter 的 String 类型


return _methodChannel.invokeMethod('getSdkVersion');


}


但当返回非基本类型时,返回值就需要进行转换,例如开启音频接口由于可能有多种结果,所以返回值是枚举类型ResultCode,如果直接按照以下写法实现将会报错:Future<ResultCode> startAudio() {


return _methodChannel.invokeMethod('startAudio');// 错误:返回值为 int 不会自动转换 Flutter 的枚举类型


}


需要增加转换逻辑,例如:Future<ResultCode> startAudio() {


return _methodChannel.invokeMethod('startAudio').then((value) {


return ResultCodeConverter.fromValue(value).e as T; // ResultCodeConverter 为将 int 转换 ResultCode 的工具类


});


}


建议:由于 SDK 中存在大量的返回 ResultCode 的方法,在每个接口实现处都增加转换代码繁琐且冗余,所以我们对于这种情况可以提取一个公共模板方法,能很大程度提升代码简洁度,例如:Future<T>_invokeMethod<T>(String method, [Map<String, dynamic> arguments]) {


if (T == ResultCode) { // 判断当前范型为 ResultCode 时,增加转换逻辑


return _methodChannel.invokeMethod(method, arguments).then((value) {


return ResultCodeConverter.fromValue(value).e as T;


});


} else { // 其他可以自动转换的情况则返回调用结果


return _methodChannel.invokeMethod(method, arguments);


}


}

以上是 Flutter 调用原生层的流程,那当原生层需要回调事件给 Flutter 层我们应该怎么做呢?这时就需要利用 EventChannel 来实现。先看下 EventChannel 的基本流程:

  1. 原生层调用 setStreamHandler(iOS 为-[FlutterEventChannel setStreamHandler:])注册 Handler 实现;

  2. EventChannel 初始化完成后,通过 StreamHandler 的 onListen(iOS 为-[FlutterStreamHandler onListenWithArguments:eventSink:])回调接口获取 eventSink 引用并保存;

  3. Flutter 层调用 EventChannel 的 receiveBroadcastStream 注册监听;

  4. 原生层通过调用 eventSink 发送事件消息。

建议:EventChannel 由于是数据流通信,跟 MethodChannel 不同之处在于没有封装出针对方法回调的模型,但目前 SDK 中原生层向 Flutter 层均为方法回调,所以我们将回调数据组装成特定格式的键值对,如:{


"methodName": xxxx, // 回调方法名


"data": [xxxx,xxxx...] // 回调参数列表


}


然后在 Flutter 层进行统一解析处理:void setEventHandler(RtcEngineEventHandler handler) {


_handler = handler;


...


_eventChannel.receiveBroadcastStream().listen((event) {


final eventMap = Map<dynamic, dynamic>.from(event);


final methodName = eventMap['methodName'] as String;


final data = List<dynamic>.from(eventMap['data']);


_handler?.process(methodName, data);


});


}

至此通过以上方案,已经可以封装原生 SDK 的绝大部分功能以 Flutter SDK 形式提供出去了。但还剩一个重要的问题需要解决,那就是如何设置原生层视图的逻辑。

#3

设置原生视图

由于 Flutter 提供的平台通道方案本质上是以字节流的方式在线程间传递数据,所以对于原生层视图等非序列化的对象是不支持的。Flutter 在如何内嵌原生层视图的问题上,提供了平台视图(Platform-views)方案,开发者可以在 Flutter 层创建原生视图的映射(iOS 为 UiKitView,Android 为 AndroidView),并嵌入到 Widget 中。

那如何将生成的原生视图对象传递给原生层 SDK?在 Flutter 创建原生视图后,会返回视图对应唯一的 id,所以最直观的方法就是在 id 返回后,分别在原生层与 Flutter 层生成对应的 MethodChannel,组成键值对缓存起来,在调用时通过 id 查找 MethodChannel,然后通过 MethodChannel 传递方法调用消息。但这样做有两个明显缺陷:

  1. MethodChannel 没有与 Widget 直接关联,在 Widget 销毁时需要手动清除键值对中的 MethodChannel;

  2. 采用 id 作为原生视图的标识,由于缺少有效性检查,可能导致调用到无效 MethodChannel 抛出异常。

并且通常原生 SDK 方法中是需要原生视图作为参数传入,但由于只能通过与视图对应的 MethodChannel 才能在原生层访问到对应的原生视图对象,导致没法直接在 Flutter 层设计出类似原生 SDK 的方法。

建议:Pano Flutter SDK 中我们为了尽量保持与原生 SDK 的接口一致性,采取了一种曲线救国的方案。在创建渲染视图RtcSurfaceView(StatefulWidget)后,回调返回保存了 MethodChannel 的 ViewModel 对象:


class RtcSurfaceViewModel {


final MethodChannel _methodChannel;



Future<T> invokeMethod<T>(String method, [Map<String, dynamic> arguments]) {


if (T == ResultCode) {


return _methodChannel.invokeMethod(method, arguments).then((value) {


return ResultCodeConverter.fromValue(value).e as T;


});


} else {


return _methodChannel.invokeMethod(method, arguments);


}


}



RtcSurfaceViewModel(this._methodChannel);


}


然后按照需要原生视图的 SDK 方法,定义出对应的 Flutter 层接口,接收 ViewModel 作为参数,方法实现调用 ViewModel 的 MethodChannel 传递方法消息,例如开启视频时调用 startVideo 接口定义如下:Future<ResultCode> startVideo(RtcSurfaceViewModel viewModel,


{RtcRenderConfig config}) {


config ??= RtcRenderConfig();


return viewModel.invokeMethod('startVideo', {'config': config.toJson()});


}


在原生层视图对应的 MethodChannel 接收到方法调用,通过原生层内部缓存的 engine 对象,调用对应的 SDK 方法(如 startVideo),传入原生层视图完成接口调用。这样做,一方面让 MethodChannel 与 Widget 关联,另一方面在接口调用上也使用 ViewModel 对象保证了传值的有效性。并且接口上也基本与原生 SDK 保持了一致性,降低了对接 SDK 的开发人员的理解成本,也兼顾了代码的维护成本。

#4

结语

现今广大开发往往会遇到各种各样跨平台开发的需求或问题,而拍乐云一直以来坚持以开发者为先,和用户在一起。Pano Flutter SDK 全部开源,你可以通过 GitHub(https://github.com/PanoVideo/PanoRtc-Flutter或 Giteehttps://gitee.com/pano-video/panortc-flutter )查看完整源码。通过本篇介绍 Pano Flutter SDK 的跨平台 SDK 设计与实践经验,希望能给大家带来一些帮助与启发。

用户头像

拍乐云Pano

关注

Be Sharp,be simple 2020.06.28 加入

我们是一家由顶级音视频团队构建的实时通信Paas云服务公司,在音视频领域拥有超过二十年的技术积累。 我们通过提供极简、稳定和安全的SDK服务,让你的应用轻松实现音视频通话、互动白板、互动直播等能力。

评论

发布
暂无评论
Pano Flutter SDK 设计经验与实践浅谈