FlutterChannel传输数据全局监听

背景

Flutter项目中,免不了和原生打交道,很多能力最终还是需要原生接口来提供,比如获取App沙盒路径、点击震动反馈、拍摄、访问相册、应用生命周期等。Flutter使用FlutterChannel来提供这个能力,至于FlutterChannel如何实现,如何应用不是本文的重点,本文的重点是探讨如何来全局监听这些FlutterChannel,并形成一个性能指标。比如传输耗时,传输数据量大小,传输是否报错等,如下图中橙色框内所示。通过这个监听生成的数据为定向优化项目打下基础。

前置知识点

SystemChannels简述

Flutter底层也提供了一系列的SystemChannels,这个系统通道提供一些最基本的能力。下面我简单举例来说明,整个系统通过列表,大家可以去源码(路径src/services/system_channels.dart)中查看。

  1. 比如想监听应用生命周期变化,就可以直接用系统通道,没必要自己写。
arduino 复制代码
// 定义 
static const BasicMessageChannel<String?> lifecycle = BasicMessageChannel<String?>(
    'flutter/lifecycle',
    StringCodec(),
);

// 使用
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
  1. 比如想监听到系统内存压力,也可以直接用系统通道;Flutter项目监听到这个信号,就可以去释放一些内存以确保App不会因为内存占用太高而被系统杀掉。
dart 复制代码
// 定义 src/services/system_channels.dart
static const BasicMessageChannel<Object?> system = BasicMessageChannel<Object?>(
    'flutter/system',
    JSONMessageCodec(),
);

// 使用
SystemChannels.system.setMessageHandler(
    (dynamic message) => handleSystemMessage(message as Object));
    
 
Future<void> handleSystemMessage(Object systemMessage) async {
  final Map<String, dynamic> message = systemMessage as Map<String, dynamic>;
  final String type = message['type'] as String;
  switch (type) {
    case 'memoryPressure':
      handleMemoryPressure();
  }
  return;
} 

对于一些业务型的的通道,官方没有提供,可以自己写,也可以使用第三方提供的。不再赘述。

ServicesBinding

在阅读源码是时候,先看他注释,使用默认消息信使defaultBinaryMessenger来监听平台消息。很容易想到,我们只要实现ServicesBinding混入,并复写这个默认消息信使就可以达到监听的目的了。

简单说说xxxBinging

WidgetsFlutterBinding通过混入7大xxxBinging,为Flutter提供了手势、帧调度、原生平台消息、绘制、语义(比如盲人点击发声)、渲染、组件树七大能力。

scala 复制代码
class WidgetsFlutterBinding extends BindingBase with 
GestureBinding, 
SchedulerBinding, 
ServicesBinding, 
PaintingBinding,
SemanticsBinding, 
RendererBinding, 
WidgetsBinding 

本文主要说下ServicesBinding,提供和原生交互的能力,并且默认初始化了一些通道,比如前文中提到的应用生命周期事件和内存压力警告其实都在这里实现了。

消息信使 BinaryMessenger

BinaryMessengerFlutter框架给我们提供的唯一一个用于从dart到原生消息转换的工具,所有的channel都是基于BinaryMessenger进行二次包装的。可以理解为一个数据协议,可以用来传递数据。

默认消息信使

查看源码,可以看到Flutter在初始化ServicesBinding的时候,创建了一个默认的消息信使用来转换数据。这个默认消息信使名字叫defaultBinaryMessenger

defaultBinaryMessenger是定义在lib/src/services/binding.dart:342中的_DefaultBinaryMessenger类一个实例。如下图所示。

那继续来看看_DefaultBinaryMessenger类的具体实现吧。

我们主要看其中的两个方法,我们需要重写下面的两个方法,然后就可以很简单的全局捕获FlutterChannel的数据了。

  1. dart发送消息到native端
scss 复制代码
Future<ByteData?> send(String channel, ByteData? message) 
  1. dart接收原生的消息
arduino 复制代码
void setMessageHandler(String channel, MessageHandler? handler)

有了前面的小知识点,话不多说,我们来看看如何全局监听FlutterChannel

FlutterChannel全局监听实现

新建一个ChannelObserverServicesBinding混入。

并且实现系统的BindingBaseServicesBinding。为什么实现ServicesBinding,是因为其提供和原生交互的能力。BinaryMessengerProxy是我们重新的消息信使,下文会有详细说明。

typescript 复制代码
mixin ChannelObserverServicesBinding on BindingBase, ServicesBinding {
  @override
  BinaryMessenger createBinaryMessenger() {
    return BinaryMessengerProxy(super.createBinaryMessenger());
  }
}

新建一个ChannelObserverBinding

继承自WidgetsFlutterBinding,并且实现第一个新建的mixin。这个类主要提供给main函数调用的,用来初始化我们写的监控方法。

scala 复制代码
class ChannelObserverBinding extends WidgetsFlutterBinding
    with ChannelObserverServicesBinding {
  static WidgetsBinding ensureInitialized() {
    ChannelObserverBinding();
    return WidgetsBinding.instance;
  }
}

main方法调整

新增初始化

csharp 复制代码
void main() {
  ChannelObserverBinding.ensureInitialized();
  runApp(const App());
}

以上三步的目的很简单,在Flutter初始化7大混入xxxBinging的同时,也初始化我们自己用来监听。

显示BinaryMessengerProxy

在第1步的时候,大家可能看到了BinaryMessengerProxy这个类,这个就是我们继承自BinaryMessenger,并重写其方法,用例捕获我们需要的信息。

4.1 重写send方法,每当我们在Flutter端调用原生方法,都会进入这个函数,参数说明

channel就是FlutterChannel的名字,

message就是传递的二进制参数,其实这个参数我们可以很简单的解开。因为这个二进制格式基本上是按照以下方法进行编码。当然会存在自定义的编码解码器,需要注意一下就行了。

scala 复制代码
class BinaryMessengerProxy extends BinaryMessenger {
  @override
  Future<ByteData?> send(String channel, ByteData? message) async {
    final ChannelInfo? info =
        _parseChannelInfo(channel, message, InterceptType.SEND);
    ByteData? result = await origin.send(channel, message);
    _parseResult(info, result);
    return result;
  }

...
}
4.2 重新setMessageHandler方法

参数channelFlutterChannel的名字

handler为注册接收原生消息的回调

大家可以看到,这边新建了一个匿名函数wrapper, 这个函数主要的目的是给handler回调加一个钩子,这样在这个handler需要触发的时候,我们可以全局加上我们需要的监听代码。这个很重要的。

scala 复制代码
class BinaryMessengerProxy extends BinaryMessenger {
  @override
void setMessageHandler(
    String channel, Future<ByteData?>? Function(ByteData? message)? handler) {
    
  wrapper(ByteData? message) {
    final ChannelInfo? info =
        _parseChannelInfo(channel, message, InterceptType.PLATFORM_MESSAGE);
    _parseResult(info, message);

    return handler?.call(message);
  }
  origin.setMessageHandler(channel, wrapper)
}

...
}

通过以上的步骤,其实我们已经可以很简单的对FlutterChannel进行了捕获,比如获取数据量的大小,执行需要的时间,FlutterChannel的方向等数据。

如下是一些测试打印日志

yaml 复制代码
flutter: 通道数据:{channelName: flutter/platform, timeCost: 1, inputSize: 75, outputSize: 6, interceptType: 0, identifyType: 0, arguments: HapticFeedbackType.lightImpact, methodName: HapticFeedback.vibrate}
flutter: 通道数据:{channelName: dev.flutter.pigeon.PathProviderApi.getDirectoryPath, timeCost: 1, inputSize: 7, outputSize: 95, interceptType: 0, identifyType: 1, arguments: null, methodName: null}
flutter: 通道数据:{channelName: leaf.com/testChannel/json, timeCost: 0, inputSize: 63, outputSize: 63, interceptType: 1, identifyType: 1, arguments: {"key1":"value1","key3":"value3","key2":"value2"}, methodName: null}
4.3 源码

其实主要的源码已经说明,剩下的是解析和统计,点击获取

相关推荐
AiFlutter20 小时前
Flutter之Package教程
flutter
Mingyueyixi1 天前
Flutter Spacer引发的The ParentDataWidget Expanded(flex: 1) 惨案
前端·flutter
crasowas1 天前
Flutter问题记录 - 适配Xcode 16和iOS 18
flutter·ios·xcode
老田低代码2 天前
Dart自从引入null check后写Flutter App总有一种难受的感觉
前端·flutter
AiFlutter3 天前
Flutter Web首次加载时添加动画
前端·flutter
ZemanZhang4 天前
Flutter启动无法运行热重载
flutter
AiFlutter5 天前
Flutter-底部选择弹窗(showModalBottomSheet)
flutter
帅次5 天前
Android Studio:驱动高效开发的全方位智能平台
android·ide·flutter·kotlin·gradle·android studio·android jetpack
程序者王大川5 天前
【前端】Flutter vs uni-app:性能对比分析
前端·flutter·uni-app·安卓·全栈·性能分析·原生