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/...)或Gitee(https://gitee.com/pano-video/... )查看残缺源码。通过本篇介绍 Pano Flutter SDK 的跨平台 SDK 设计与实践经验,心愿能给大家带来一些帮忙与启发。粗体粗体粗体