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 的基本流程:
原生层调用
setStreamHandler
(iOS 为-[FlutterEventChannel setStreamHandler:]
)注册 Handler 实现;EventChannel 初始化完成后,通过 StreamHandler 的 onListen(iOS 为
-[FlutterStreamHandler onListenWithArguments:eventSink:]
)回调接口获取 eventSink 引用并保存;Flutter 层调用 EventChannel 的 receiveBroadcastStream 注册监听;
原生层通过调用 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 传递方法调用消息。但这样做有两个明显缺陷:
MethodChannel 没有与 Widget 直接关联,在 Widget 销毁时需要手动清除键值对中的 MethodChannel;
采用 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)或 Gitee(https://gitee.com/pano-video/panortc-flutter )查看完整源码。通过本篇介绍 Pano Flutter SDK 的跨平台 SDK 设计与实践经验,希望能给大家带来一些帮助与启发。
评论