前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter Chanel通信流程

Flutter Chanel通信流程

原创
作者头像
杨充
修改2021-08-27 11:14:22
5.1K0
修改2021-08-27 11:14:22
举报
文章被收录于专栏:lib库lib库
目录介绍
  • 01.flutter和原生之间交互
  • 02.MethodChanel流程
  • 03.MethodChanel使用流程
  • 04.MethodChanel代码实践
  • 05.EventChannel流程
  • 06.EventChannel基本流程
  • 07.EventChannel代码实现
  • 08.BasicMessageChannel流程
  • 09.BasicMessageChannel基本流程
  • 10.BasicMessageChannel代码实现
  • 11.Channel编解码器说明
  • 12.Channel通信可以子线程吗
  • 13.Channel通信传递稳定性
  • 14.onActivityResult如何实现

推荐

01.flutter和原生之间交互

1.1 交互简单介绍
  • 官方给的通信方式
    • 看图片,channel通信方式
    • 从底层来看,Flutter和平台端通信的方式是发送异步的二进制消息,该基础通信方式在Flutter端由BinaryMessages来实现, 而在Android端是一个接口BinaryMessenger,其具体实现为FlutterNativeView,在iOS端是一个协议 FlutterBinaryMessenger,FlutterViewController遵守并实现了这个协议。
  • flutter可以与native之间进行通信,帮助我们使用native提供的能力。
    • 通信是双向的,我们可以从Native层调用flutter层的dart代码,同时也可以从flutter层调用Native的代码。
  • 我们需要使用Platform Channels APIs进行通信,主要包括下面三种:
    • MethodChannel:用于传递方法调用(method invocation)
    • EventChannel:用于事件流的发送(event streams)
    • BasicMessageChannel:用于传递字符串和半结构化的消息,这里什么叫做半结构化?下面会解释……
  • channel通信是异步还是同步的
    • 为了保证用户界面在交互过程中的流畅性,无论是从Flutter向Native端发送消息,还是Native向Flutter发送消息都是以异步的形式进行传递的。那为何不使用同步来操作,下面会说到……
  • 几种channel应用场景分析
    • MethodChannel使用场景:无论是Flutter端还是Native端都可以通过MethodChannel向对方平台发送两端提前定义好的方法名来调用对方平台相对应的消息处理逻辑并且带回返回值给被调用方。
    • EventChannel的使用场景:更侧重于Native平台主动向Flutter平台,单向给Flutter平台发送消息,Flutter无法返回任何数据给Native端,EventChannel描述是单通的。可以类比Android里面的广播……
    • BasicMessageChannel的使用场景:比如flutter想拍照,拍完照后的图片路径需要传给flutter,照片的路径发送可以使用BasicMessageChannel.Reply回复,也可以使用sendMessage主动再发一次消息。个人认为接收消息并回复消息属于一次通信,所以倾向于使用BasicMessageChannel.Reply。
  • 混合开发通常用那种channel
    • 只是混合开发通常涉及到两端频繁通信,个人更加倾向使用BasicMessageChannel,不分主客,使用和通信更方便。
1.2 核心类重点说明
  • MethodCall
    • 方法调用Java层封装,主要是数据类
  • MethodChannel
    • 这个主要用户和dart进行方法通信,类
  • MethodCallHandler
    • 这个java层处理dart层时间的接口,在通讯协议中属于上层接口,接口
  • BinaryMessageHandler
    • java层和dart层通讯的最底层抽象接口,面向二进制数据包,接口
  • DartMessenger
    • 最底层用于接收JNI发送过来的数据。实现类
  • DartExecutor
    • 配置、引导并开始执行Dart代码。BinaryMessenger的具体实现类
  • FlutterView
    • NA用来承载flutter的容器view
  • IncomingMethodCallHandler
    • BinaryMessageHandler的实现类,用户接收底层发送过来的数据包,然后转发给MethodCallHandler,并对MethodCallHandler 发送过的结果进行打包发送给dart层。实现类
  • FlutterJNI
    • JNI层的封装用于跟底层引擎侧进行通讯

02.MethodChannel流程

  • 其中最常用的是MethodChanel,MethodChanel的使用与在Android的JNI调用非常类似,但是MethodChanel更加简单,而且相对于JNI的同步调用MethodChanel的调用是异步的:
    • image
      image
  • 从flutter架构图上可以看到,flutter与native的通信发生在Framework和Engine之间,framewrok内部会将MethodChannel以BinaryMessage的形式与Engine进行数据交换。

03.MethodChanel使用流程

3.1 flutter调用native
  • flutter调用native步骤
    • native 使用MethodChannel#setMethodCallHandler注册回调
    • flutter 通过MethodChannel#invokeMethod发起异步调用
    • native 调用native方法通过Result#success返回Result,出错时返回error
    • flutter 收到native返回的Result
  • 如图所示
    • image
      image
3.2 native调用flutter
  • native调用flutter
    • 与flutter调用native的顺序完全一致,只是native与flutter角色反调
  • 如图所示
    • image
      image
  • NA端使用MethodChannel
    • 首先定义Channel名称,需要保证是唯一的,在Flutter端需要使用同样的名称来创建MethodChannel。如果名称不一样,则会导致匹配不上……
    • 第一个参数:是messenger,类型是BinaryMessenger,是一个接口,代表消息信使,是消息发送与接收的工具;
    • 第二个参数:是name,就是Channel名称,和flutter定义的要一样;
    • 第三个参数:是codec,类型是MethodCodec,代表消息的编解码器,如果没有传该参数,默认使用StandardMethodCodec。

04.MethodChanel代码实践

4.1 native调用flutter
  • 定义好了MethodChannel之后调用setMethodCallHandler()方法设置消息处理回调,参数是MethodHandler类型,需要实现它的onMethodCall()方法。onMethodCall()方法有两个参数methodCall和result,methodCall记录了调用的方法信息,包括方法名和参数,result用于方法的返回值,可以通过result.success()方法返回信息给Flutter端。private void createChannel() { nativeChannel = new MethodChannel(binaryMessenger, METHOD_CHANNEL, StandardMethodCodec.INSTANCE); // 注册Handler实现 nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(@NonNull MethodCall methodCall, @NonNull MethodChannel.Result result) { if ("android".equals(methodCall.method)) { //接收来自flutter的指令 String flutter = methodCall.argument("flutter"); //返回给flutter的参数 result.success("Na收到指令"); } } }); }
  • 可以通过invokeMethod方法让NA执行调用flutter方法。那么执行了flutter方法后需要回传数据,这个时候就需要用到Result接口呢,代码如下所示:HashMap<String , String> map = new HashMap<>(); map.put("invokeKey","你好,这个是从NA传递过来的数据"); //nativeChannel.resizeChannelBuffer(100); nativeChannel.invokeMethod("getFlutterResult", map , new MethodChannel.Result() { @SuppressLint("SetTextI18n") @Override public void success(@Nullable Object result) { tvContent.setText("测试内容:"+result); }
代码语言:txt
复制
    @SuppressLint("SetTextI18n")
代码语言:txt
复制
    @Override
代码语言:txt
复制
    public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
代码语言:txt
复制
        tvContent.setText("测试内容:flutter传递给na数据传递错误");
代码语言:txt
复制
    }
代码语言:txt
复制
    @Override
代码语言:txt
复制
    public void notImplemented() {
代码语言:txt
复制
    }
代码语言:txt
复制
});
代码语言:txt
复制
```
  • 事件接收处理端
    • 接收处理回调时onMethodCall(MethodCall call, MethodChannel.Result result)通过methodCall接收事件发送者传递回来的信息,通过Result把处理完的结果发送给事件发送方。
    • 通过methodCall.method:来区分不同函数名(方法)名以执行不同的业务逻辑,
    • 通过methodCall.hasArgument("key"):判断是否有某个key对应的value
    • 通过methodCall.argument("key"):获取key对应的value值
    • 通过result.success(object):把处理完的结果返回给事件发送方
  • 事件发送端
    • 处理事件发送方通过methodChannel.invokeMethod("方法名","要传递的参数")把需要传递的参数传递给事件监听者。 其中
    • 方法名:不能为空
    • 要传递的参数:可以为空,若不为空则必须为可Json序列化的对象。
    • callback:可以为空,若不为空则表示执行了flutter方法后的回调监听状态
4.2 flutter调用native
  • Flutter使用MethodChannel
    • 在Flutter端同样需要定义一个MethodChannel,使用MethodChannel需要引入services.dart包,Channel名称要和Android端定义的相同。static const method = const MethodChannel('com.ycbjie.android/method');
  • 添加监听NA调用flutter方法的监听,flutter代码是setMethodCallHandler方法实现。return则表示flutter回传给NA的数据操作。 method.setMethodCallHandler(nativeCallHandler);
代码语言:txt
复制
  // 注册方法,等待被原生通过invokeMethod唤起
代码语言:txt
复制
  Future<dynamic> nativeCallHandler(MethodCall methodCall) async {
代码语言:txt
复制
    switch (methodCall.method) {
代码语言:txt
复制
      case "getFlutterResult":
代码语言:txt
复制
      //获取参数
代码语言:txt
复制
        String paramsFromNative = await methodCall.arguments["invokeKey"];
代码语言:txt
复制
        print("原生android传递过来的参数为------ $paramsFromNative");
代码语言:txt
复制
        return "你好,这个是从flutter回传给NA的数据";
代码语言:txt
复制
        break;
代码语言:txt
复制
    }
代码语言:txt
复制
  }
代码语言:txt
复制
```
  • flutter是如何给NA发送消息的呢,直接调用invokeMethod方法,代码如下所示 Future<Null> _jumpToNativeWithParams1() async { Map<String, String> map = { "flutter": "这是一条来自flutter的参数" }; String result = await method.invokeMethod('android', map); print(result); }

05.EventChannel流程

  • EventChannel用于从native向flutter发送通知事件,例如flutter通过其监听Android的重力感应变化等。与MethodChannel不同,EventChannel是native到flutter的单向调用,调用是多播(一对多)的,可以类比成Android的Brodecast广播。

06.EventChannel基本流程

  • 照例先看一下API使用的基本流程:
    • nativeEventChannel#setStreamHandler注册Handler实现
    • nativeEventChannel初始化结束后,在StreamHandler#onLister回调中获取EventSink引用并保存
    • flutterEventChannel#receiveBroadcastStream注册listener,建立监听
    • native使用EventSink#sucess发送通知事件
    • flutter接受到事件通知
    • native通知结束时调用endOfStream结束
  • 如图所示
    • image
      image

07.EventChannel代码实现

  • flutter端
    • 创建EventChannel,注册“包名/标识符”的channel名
    • 通过StreamSubscription#listen注册listener,其中cancelOnError参数表示遇到错误时是否自动结束监听class _MyHomePageState extends State<MyHomePage> { static const EventChannel _channel = const EventChannel('com.example.eventchannel/interop');
代码语言:txt
复制
  StreamSubscription _streamSubscription;
代码语言:txt
复制
  String _platformMessage;
代码语言:txt
复制
  void _enableEventReceiver() {
代码语言:txt
复制
    _streamSubscription = _channel.receiveBroadcastStream().listen(
代码语言:txt
复制
        (dynamic event) {
代码语言:txt
复制
          print('Received event: $event');
代码语言:txt
复制
          setState(() {
代码语言:txt
复制
            _platformMessage = event;
代码语言:txt
复制
          });
代码语言:txt
复制
        },
代码语言:txt
复制
        onError: (dynamic error) {
代码语言:txt
复制
          print('Received error: ${error.message}');
代码语言:txt
复制
        },
代码语言:txt
复制
        cancelOnError: true);
代码语言:txt
复制
  }
代码语言:txt
复制
  void _disableEventReceiver() {
代码语言:txt
复制
    if (_streamSubscription != null) {
代码语言:txt
复制
      _streamSubscription.cancel();
代码语言:txt
复制
      _streamSubscription = null;
代码语言:txt
复制
    }
代码语言:txt
复制
  }
代码语言:txt
复制
  @override
代码语言:txt
复制
  initState() {
代码语言:txt
复制
    super.initState();
代码语言:txt
复制
    _enableEventReceiver();
代码语言:txt
复制
  }
代码语言:txt
复制
  @override
代码语言:txt
复制
  void dispose() {
代码语言:txt
复制
    super.dispose();
代码语言:txt
复制
    _disableEventReceiver();
代码语言:txt
复制
  }
代码语言:txt
复制
```
  • native(android)端
    • 通过EventChannel#setStreamHandler注册Handler实现
    • 初始化完成后,获取eventSink引用并保存
    • eventSink发送事件通知
    • 通知结束时调用event#endOfStream,此时onCancel会被调用
    • 必要时,可通过evnetSink#error发送错误通知,flutter的StreamSubscription#onError会收到通知class MainActivity: FlutterActivity() { private lateinit var channel: EventChannel var eventSink: EventSink? = null
代码语言:txt
复制
    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
代码语言:txt
复制
        GeneratedPluginRegistrant.registerWith(flutterEngine)
代码语言:txt
复制
        channel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.eventchannel/interop")
代码语言:txt
复制
        channel.setStreamHandler(
代码语言:txt
复制
                object : StreamHandler {
代码语言:txt
复制
                    override fun onListen(arguments: Any?, events: EventSink) {
代码语言:txt
复制
                        eventSink = events
代码语言:txt
复制
                        Log.d("Android", "EventChannel onListen called")
代码语言:txt
复制
                        Handler().postDelayed({
代码语言:txt
复制
                            eventSink?.success("Android")
代码语言:txt
复制
                            //eventSink?.endOfStream()
代码语言:txt
复制
                            //eventSink?.error("error code", "error message","error details")
代码语言:txt
复制
                        }, 500)
代码语言:txt
复制
                    }
代码语言:txt
复制
                    override fun onCancel(arguments: Any?) {
代码语言:txt
复制
                        Log.w("Android", "EventChannel onCancel called")
代码语言:txt
复制
                    }
代码语言:txt
复制
                })
代码语言:txt
复制
    }
代码语言:txt
复制
}
代码语言:txt
复制
```

08.BasicMessageChannel流程

  • BasicMessageChannel用于在flutter和native互相发送消息,一方给另一方发送消息,收到消息之后给出回复。

09.BasicMessageChannel基本流程

  • flutter向native发送消息
    • flutter创建BasicMessageChannel
    • native通过BasicMessageChannel#MessageHandler注册Handler
    • flutter通过BasicMessageChannel#send发送消息
    • nativeBasicMessageChannel#MessageHandler#onMessage中接收消息,然后reply
  • 如图所示
    • image
      image
  • native向flutter发送消息
    • 流程也是一样的,只是将flutter与native反调
  • 如图所示
    • image
      image

10.BasicMessageChannel代码实现

10.1flutter端
  • flutter需要完成以下工作
    • 创建BasicMessageChannel
    • 通过BasicMessageChannel#send发送消息
  • 相对与其他Channel类型的创建,MessageChannel的创建除了channel名以外,还需要指定编码方式:BasicMessageChannel(String name, MessageCodec<T> codec, {BinaryMessenger binaryMessenger})
  • 发送的消息会以二进制的形式进行处理,所以要针对不同类型的数进行二进制编码
    • 编码类型 消息格式
    • BinaryCodec 发送二进制消息时
    • JSONMessageCodec 发送Json格式消息时
    • StandardMessageCodec 发送基本型数据时
    • StringCodec 发送String类型消息时
  • 代码class _MyHomePageState extends State<MyHomePage> { static const _channel = BasicMessageChannel('com.ycbjie.android/basic', StringCodec());
代码语言:txt
复制
  String _platformMessage;
代码语言:txt
复制
  void _sendMessage() async {
代码语言:txt
复制
    final String reply = await _channel.send('Hello World form Dart');
代码语言:txt
复制
    print(reply);
代码语言:txt
复制
  }
代码语言:txt
复制
  @override
代码语言:txt
复制
  initState() {
代码语言:txt
复制
    super.initState();
代码语言:txt
复制
    // Receive messages from platform
代码语言:txt
复制
    _channel.setMessageHandler((String message) async {
代码语言:txt
复制
      print('Received message = $message');
代码语言:txt
复制
      setState(() => _platformMessage = message);
代码语言:txt
复制
      return 'Reply from Dart';
代码语言:txt
复制
    });
代码语言:txt
复制
    // Send message to platform
代码语言:txt
复制
    _sendMessage();
代码语言:txt
复制
  }
代码语言:txt
复制
```
10.2 native(android)端
  • android端完成以下工作:
    • 创建BasicMessageChannel
    • 通过setHandler注册MessageHandler
    • MessageHandler#onMessage回调中接收到message后,通过reply进行回复
  • 代码class MainActivity: FlutterActivity() { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine)
代码语言:txt
复制
        val channel = BasicMessageChannel(
代码语言:txt
复制
                flutterEngine.dartExecutor.binaryMessenger,
代码语言:txt
复制
                "com.ycbjie.android/basic", StringCodec.INSTANCE)
代码语言:txt
复制
        // Receive messages from Dart
代码语言:txt
复制
        channel.setMessageHandler { message, reply ->
代码语言:txt
复制
            Log.d("Android", "Received message = $message")
代码语言:txt
复制
            reply.reply("Reply from Android")
代码语言:txt
复制
        }
代码语言:txt
复制
        // Send message to Dart
代码语言:txt
复制
        Handler().postDelayed({
代码语言:txt
复制
            channel.send("Hello World from Android") { reply ->
代码语言:txt
复制
                Log.d("Android", "$reply")
代码语言:txt
复制
            }
代码语言:txt
复制
        }, 500)
代码语言:txt
复制
    }
代码语言:txt
复制
}
代码语言:txt
复制
```

11.Channel编解码器说明

11.1 什么是消息编解码器
  • 什么是消息编解码器
    • 在Flutter和平台间进行相互通信了,但是收发的数据都是二进制的,这就需要开发者考虑更多的细节,如字节顺序(大小端)和怎么表示更高级的消息类型,如字符串,map等。
    • 因此,Flutter 还提供了消息编解码器(Codec), 用于高级数据类型(字符串,map等)和二进制数据(byte)之间的转换,即消息的序列化和反序列化。
  • 消息编解码器种类有哪些
    • MethodCodec:方法传递的编解码器抽象,接口
    • JSONMethodCodec:MethodCodec的实现类,会把数据打包成json结构发送给dart,类
    • StandardMethodCodec:MethodCodec的实现类,会把数据打包成默认格式发送给dart,类
11.2 四种消息编解码器类型
  • BinaryCodec
    • MessageCodec的实现类,直接发送二进制数据
    • BinaryCodec是最为简单的一种Codec,因为其返回值类型和入参的类型相同,均为二进制格式(Android中为ByteBuffer,iOS中为NSData)。实际上,BinaryCodec在编解码过程中什么都没做,只是原封不动将二进制数据消息返回而已。或许你会因此觉得BinaryCodec没有意义,但是在某些情况下它非常有用,比如使用BinaryCodec可以使传递内存数据块时在编解码阶段免于内存拷贝。
  • StringCodec
    • MessageCodec的实现类,负责解码和编码String类型的消息
    • 使用 UTF-8 编码格式对字符串数据进行编解码,在Android平台转换为 java.util.String 类型
  • JSONMessageCodec
    • MessageCodec的实现类,负责解码和编码Json类型的消息
    • JSONMessageCodec用于处理 JSON 数据类型(字符串型,数字型,布尔型,null,只包含这些类型的数组,和key为string类型,value为这些类型的map),在编码过程中,数据会被转换为JSON字符串,然后在使用 UTF-8 格式转换为字节型。
  • StandardMessageCodec
    • MessageCodec的实现类,负责解码和编码默认类型的消息
    • StandardMessageCodec 可以认为是 JSONMessageCodec 的升级版,能够处理的数据类型要比 JSONMessageCodec 更普遍一些,且在处理 int 型数据时,会根据 int 数据的大小来转为平台端的32位类型(int)或者是64位类型(long),StandardMessageCodec 也是 Flutter Platform channel 的默认编解码器
11.3 编码器的源码分析下
  • 首先看下MessageCodecabstract class MessageCodec<T> {
代码语言:txt
复制
  ByteData encodeMessage(T message);
代码语言:txt
复制
  T decodeMessage(ByteData message);
代码语言:txt
复制
}
代码语言:txt
复制
```
11.4 看StandardMessageCodec
  • StandardMessageCodec稍微复杂
    • StandardMessageCodec在写入数据的时候,显示写入这个数据的类型值定义,然后在写入其对应的具体值,什么意思呢?
  • 查看一下如何写入指定类型的值,代码如下所示: protected void writeValue(ByteArrayOutputStream stream, Object value) { if (value == null || value.equals(null)) { stream.write(NULL); } else if (value == Boolean.TRUE) { stream.write(TRUE); } else if (value == Boolean.FALSE) { stream.write(FALSE); } else if (value instanceof Number) { if (value instanceof Integer || value instanceof Short || value instanceof Byte) { stream.write(INT); writeInt(stream, ((Number) value).intValue()); } else if (value instanceof Long) { stream.write(LONG); writeLong(stream, (long) value); } else if (value instanceof Float || value instanceof Double) { stream.write(DOUBLE); writeAlignment(stream, 8); writeDouble(stream, ((Number) value).doubleValue()); } else if (value instanceof BigInteger) { stream.write(BIGINT); writeBytes(stream, ((BigInteger) value).toString(16).getBytes(UTF8)); } else { throw new IllegalArgumentException("Unsupported Number type: " + value.getClass()); } } else if (value instanceof String) { stream.write(STRING); writeBytes(stream, ((String) value).getBytes(UTF8)); } else if (value instanceof byte[]) { stream.write(BYTE_ARRAY); writeBytes(stream, (byte[]) value); } else if (value instanceof int[]) { stream.write(INT_ARRAY); final int[] array = (int[]) value; writeSize(stream, array.length); writeAlignment(stream, 4); for (final int n : array) { writeInt(stream, n); } } else if (value instanceof long[]) { stream.write(LONG_ARRAY); final long[] array = (long[]) value; writeSize(stream, array.length); writeAlignment(stream, 8); for (final long n : array) { writeLong(stream, n); } } else if (value instanceof double[]) { stream.write(DOUBLE_ARRAY); final double[] array = (double[]) value; writeSize(stream, array.length); writeAlignment(stream, 8); for (final double d : array) { writeDouble(stream, d); } } else if (value instanceof List) { stream.write(LIST); final List<?> list = (List) value; writeSize(stream, list.size()); for (final Object o : list) { writeValue(stream, o); } } else if (value instanceof Map) { stream.write(MAP); final Map<?, ?> map = (Map) value; writeSize(stream, map.size()); for (final Entry<?, ?> entry : map.entrySet()) { writeValue(stream, entry.getKey()); writeValue(stream, entry.getValue()); } } else { throw new IllegalArgumentException("Unsupported value: " + value); } }
  • 查看一下如何读取指定类型的值,代码如下所示: protected Object readValueOfType(byte type, ByteBuffer buffer) { final Object result; switch (type) { case NULL: result = null; break; case TRUE: result = true; break; case FALSE: result = false; break; case INT: result = buffer.getInt(); break; case LONG: result = buffer.getLong(); break; case BIGINT: { final byte[] hex = readBytes(buffer); result = new BigInteger(new String(hex, UTF8), 16); break; } case DOUBLE: readAlignment(buffer, 8); result = buffer.getDouble(); break; case STRING: { final byte[] bytes = readBytes(buffer); result = new String(bytes, UTF8); break; } case BYTE_ARRAY: { result = readBytes(buffer); break; } case INT_ARRAY: { final int length = readSize(buffer); final int[] array = new int[length]; readAlignment(buffer, 4); buffer.asIntBuffer().get(array); result = array; buffer.position(buffer.position() + 4 * length); break; } case LONG_ARRAY: { final int length = readSize(buffer); final long[] array = new long[length]; readAlignment(buffer, 8); buffer.asLongBuffer().get(array); result = array; buffer.position(buffer.position() + 8 * length); break; } case DOUBLE_ARRAY: { final int length = readSize(buffer); final double[] array = new double[length]; readAlignment(buffer, 8); buffer.asDoubleBuffer().get(array); result = array; buffer.position(buffer.position() + 8 * length); break; } case LIST: { final int size = readSize(buffer); final List<Object> list = new ArrayList<>(size); for (int i = 0; i < size; i++) { list.add(readValue(buffer)); } result = list; break; } case MAP: { final int size = readSize(buffer); final Map<Object, Object> map = new HashMap<>(); for (int i = 0; i < size; i++) { map.put(readValue(buffer), readValue(buffer)); } result = map; break; } default: throw new IllegalArgumentException("Message corrupted"); } return result; }
11.5 如何选择合适编解码器
  • 编解码的实现类并不复杂
    • 可以先了解一下这个比较能更好的理解数据传递,其实不关java上层使用那种方式,最终传递给底层数据都是固定格式,约定统一的数据格式双方才能识别出来,正常的来说用默认的编解码格式就可以了。
  • 关于四种解码器使用场景
    • BinaryCodec
      • 暂未找到使用的场景
    • StringCodec
      • 适用发送单一的字符串数据,数据量单一的情况,比如LifecycleChannelpublic void appIsInactive() { Log.v(TAG, "Sending AppLifecycleState.inactive message."); channel.send("AppLifecycleState.inactive"); }
    • JSONMessageCodec
      • 适用数据量比较复杂的情况,比如有携带多个数据字段的传递,比如KeyEventChannelpublic void keyDown(@NonNull FlutterKeyEvent keyEvent) { Map<String, Object> message = new HashMap<>(); message.put("type", "keydown"); message.put("keymap", "android"); encodeKeyEvent(keyEvent, message);
代码语言:txt
复制
      channel.send(message);
代码语言:txt
复制
    }
代码语言:txt
复制
    ```
代码语言:txt
复制
- StandardMessageCodec
    - 默认的数据编解码,绝大多数的情况下使用默认的就可以了。比如:MethodChannel,EventChannel

12.Channel通信可以子线程吗

12.1 Android发送通信信息
  • 首先看一下Android发送通信信息,主要分析入口是:nativeChannel.invokeMethod("setNum", a , null);public void invokeMethod(String method, @Nullable Object arguments, Result callback) { messenger.send( name, codec.encodeMethodCall(new MethodCall(method, arguments)), callback == null ? null : new IncomingResultHandler(callback)); }
  • 最终定位找到DartMessenger类的send方法,代码如下所示:@Override public void send( @NonNull String channel, @Nullable ByteBuffer message, @Nullable BinaryMessenger.BinaryReply callback) { Log.v(TAG, "Sending message with callback over channel '" + channel + "'"); int replyId = 0; if (callback != null) { replyId = nextReplyId++; pendingReplies.put(replyId, callback); } if (message == null) { flutterJNI.dispatchEmptyPlatformMessage(channel, replyId); } else { flutterJNI.dispatchPlatformMessage(channel, message, message.position(), replyId); } }
  • 尝试一下子线程发送消息,发现会出现崩溃new Thread(new Runnable() { @Override public void run() { nativeChannel.invokeMethod("setNum", a , null); } }).start();
  • 崩溃信息如下所示java.lang.RuntimeException: Methods marked with @UiThread must be executed on the main thread. Current thread: Thread-2574 at io.flutter.embedding.engine.FlutterJNI.ensureRunningOnMainThread(FlutterJNI.java:992) at io.flutter.embedding.engine.FlutterJNI.dispatchPlatformMessage(FlutterJNI.java:736) at io.flutter.embedding.engine.dart.DartMessenger.send(DartMessenger.java:72) at io.flutter.embedding.engine.dart.DartExecutor$DefaultBinaryMessenger.send(DartExecutor.java:370) at io.flutter.plugin.common.MethodChannel.invokeMethod(MethodChannel.java:94) at com.ycbjie.ycandroid.channel.MethodChannelActivity.test1000(MethodChannelActivity.java:302) at com.ycbjie.ycandroid.channel.MethodChannelActivity.access$000(MethodChannelActivity.java:46) at com.ycbjie.ycandroid.channel.MethodChannelActivity$1.run(MethodChannelActivity.java:98) at java.lang.Thread.run(Thread.java:818)
12.Flutter给NA发送数据
  • 从method.invokeMethod('android', map);开始分析 @optionalTypeArgs Future<T> _invokeMethod<T>(String method, { bool missingOk, dynamic arguments }) async { assert(method != null); final ByteData result = await binaryMessenger.send( name, codec.encodeMethodCall(MethodCall(method, arguments)), ); return codec.decodeEnvelope(result) as T; }
  • 最后定位到_DefaultBinaryMessenger类中的send方法 Future<ByteData> _sendPlatformMessage(String channel, ByteData message) { final Completer<ByteData> completer = Completer<ByteData>(); // ui.window is accessed directly instead of using ServicesBinding.instance.window // because this method might be invoked before any binding is initialized. // This issue was reported in #27541. It is not ideal to statically access // ui.window because the Window may be dependency injected elsewhere with // a different instance. However, static access at this location seems to be // the least bad option. ui.window.sendPlatformMessage(channel, message, (ByteData reply) { try { completer.complete(reply); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'services library', context: ErrorDescription('during a platform message response callback'), )); } }); return completer.future; }

13.Channel通信传递稳定性

  • channel传递数据是否会丢失,如何测试呢?可以模拟,Android给flutter发送1000条信息,然后flutter给Android发送1000条信息,接下来看一下如何测试:
13.1 Android给flutter发送数据
  • Android给flutter发送数据,代码如下所示int a = 0; private void test1000() { if (nativeChannel!=null){ for (int i=0 ; i<1000 ; i++){ a++; Log.i("测试数据test1000 :", a+""); nativeChannel.invokeMethod("setNum", a , new MethodChannel.Result() { @SuppressLint("SetTextI18n") @Override public void success(@Nullable Object result) { if (result==null){ return; } Log.i("测试数据:",result.toString()); }
代码语言:txt
复制
                @SuppressLint("SetTextI18n")
代码语言:txt
复制
                @Override
代码语言:txt
复制
                public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
代码语言:txt
复制
                    Log.i("测试数据异常:",errorMessage);
代码语言:txt
复制
                }
代码语言:txt
复制
                @Override
代码语言:txt
复制
                public void notImplemented() {
代码语言:txt
复制
                }
代码语言:txt
复制
            });
代码语言:txt
复制
        }
代码语言:txt
复制
    }
代码语言:txt
复制
}
代码语言:txt
复制
```
  • flutter接收数据并回传数据给Android //接受na端传递过来的方法,并做出响应逻辑处理 method.setMethodCallHandler(nativeCallHandler);
代码语言:txt
复制
  // 注册方法,等待被原生通过invokeMethod唤起
代码语言:txt
复制
  Future<dynamic> nativeCallHandler(MethodCall methodCall) async {
代码语言:txt
复制
    switch (methodCall.method) {
代码语言:txt
复制
      case "setNum":
代码语言:txt
复制
      //获取参数
代码语言:txt
复制
        int message = await methodCall.arguments;
代码语言:txt
复制
        print("原生android传递过来的参数为------ $message");
代码语言:txt
复制
        return "flutter回调数据:${message.toString()}";
代码语言:txt
复制
        break;
代码语言:txt
复制
    }
代码语言:txt
复制
  }
代码语言:txt
复制
```
13.2 查看数据稳定性和及时性
  • Android发送消息日志2021-08-26 11:58:03.837 23106-23106/com.ycbjie.ychybrid I/测试数据test1000:: 990 2021-08-26 11:58:03.837 23106-23106/com.ycbjie.ychybrid I/测试数据test1000:: 991 2021-08-26 11:58:03.837 23106-23106/com.ycbjie.ychybrid I/测试数据test1000:: 992 2021-08-26 11:58:03.837 23106-23106/com.ycbjie.ychybrid I/测试数据test1000:: 993 2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/测试数据test1000:: 994 2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/测试数据test1000:: 995 2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/测试数据test1000:: 996 2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/测试数据test1000:: 997 2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/测试数据test1000:: 998 2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/测试数据test1000:: 999 2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/测试数据test1000:: 1000
  • flutter接收Android发送数据2021-08-26 11:52:39.708 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android传递过来的参数为------ 992 2021-08-26 11:52:39.709 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android传递过来的参数为------ 993 2021-08-26 11:52:39.709 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android传递过来的参数为------ 994 2021-08-26 11:52:39.709 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android传递过来的参数为------ 995 2021-08-26 11:52:39.709 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android传递过来的参数为------ 996 2021-08-26 11:52:39.710 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android传递过来的参数为------ 997 2021-08-26 11:52:39.710 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android传递过来的参数为------ 998 2021-08-26 11:52:39.710 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android传递过来的参数为------ 999 2021-08-26 11:52:39.710 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android传递过来的参数为------ 1000
  • flutter收到消息后,回调给Android数据。Android监听回调数据,打印日志如下2021-08-26 11:58:03.964 23106-23106/com.ycbjie.ychybrid I/测试数据:: flutter回调数据:600 2021-08-26 11:58:03.964 23106-23106/com.ycbjie.ychybrid I/测试数据:: flutter回调数据:601 2021-08-26 11:58:03.964 23106-23106/com.ycbjie.ychybrid I/测试数据:: flutter回调数据:602 2021-08-26 11:58:03.965 23106-23106/com.ycbjie.ychybrid I/测试数据:: flutter回调数据:603 2021-08-26 11:58:03.965 23106-23106/com.ycbjie.ychybrid I/测试数据:: flutter回调数据:604 2021-08-26 11:58:03.965 23106-23106/com.ycbjie.ychybrid I/测试数据:: flutter回调数据:605 2021-08-26 11:58:03.965 23106-23106/com.ycbjie.ychybrid I/测试数据:: flutter回调数据:606 2021-08-26 11:58:03.966 23106-23106/com.ycbjie.ychybrid I/测试数据:: flutter回调数据:607
  • 然后再看一波打印日志,如下所示2021-08-26 12:07:09.158 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 1 2021-08-26 12:07:09.237 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:1 2021-08-26 12:07:09.240 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 2 2021-08-26 12:07:09.241 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 3 2021-08-26 12:07:09.241 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:2 2021-08-26 12:07:09.241 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:3 2021-08-26 12:07:09.241 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 4 2021-08-26 12:07:09.241 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:4 2021-08-26 12:07:09.241 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 5 2021-08-26 12:07:09.241 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:5 2021-08-26 12:07:09.242 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 6 2021-08-26 12:07:09.242 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:6 2021-08-26 12:07:09.242 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 7 2021-08-26 12:07:09.242 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:7
代码语言:txt
复制
2021-08-26 12:07:09.272 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 131
代码语言:txt
复制
2021-08-26 12:07:09.273 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:131
代码语言:txt
复制
2021-08-26 12:07:09.273 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 132
代码语言:txt
复制
2021-08-26 12:07:09.273 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:132
代码语言:txt
复制
2021-08-26 12:07:09.273 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 133
代码语言:txt
复制
2021-08-26 12:07:09.273 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:133
代码语言:txt
复制
2021-08-26 12:07:09.273 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 134
代码语言:txt
复制
2021-08-26 12:07:09.273 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:134
代码语言:txt
复制
2021-08-26 12:07:09.273 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 135
代码语言:txt
复制
2021-08-26 12:07:09.274 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:135
代码语言:txt
复制
2021-08-26 12:07:09.274 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 136
代码语言:txt
复制
2021-08-26 12:07:09.274 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:136
代码语言:txt
复制
2021-08-26 12:07:09.274 24237-24990/com.ycbjie.ychybrid I/flutter: 测试数据,flutter接收NA数据: 137
代码语言:txt
复制
2021-08-26 12:07:09.274 24237-24237/com.ycbjie.ychybrid I/测试数据,NA接收flutter回调:: flutter回调数据:137
代码语言:txt
复制
```
  • 因此查看日志可以得知,传递数据保证了数据的时效性,发送消息和接收消息是一一对应。并没有失败的情况,因此传递数据是稳定的。
  • 重点说明,有小伙伴有疑惑,你这遍历1000次,每次传递都是int值,那实际开发中可能传递大json,数据量大的情况会怎样,这个下面会说到……

14.onActivityResult如何实现

  • 先说一个场景
    • 在开发中我们经常会遇到关闭当前页面的同时返回给上一个页面数据的场景,在Android中是通过startActivityForResult和onActivityResult()实现的。
    • 而纯Flutter页面之间可以通过在Navigator.of(context).pop()方法中添加参数来实现,那么对于Flutter页面和Android原生页面之间如何在返回上一页时传递数据呢,通过MethodChannel就可以实现。
14.1 Flutter页面返回Android原生页面
  • 在Flutter端调用原生的返回方法就可以了,首先在Flutter页面添加一个按钮,点击按钮返回原生页面,代码如下:new Padding( padding: const EdgeInsets.only( left: 10.0, top: 10.0, right: 10.0), child: new RaisedButton( textColor: Colors.black, child: new Text('返回上一界面,并携带数据'), onPressed: () { Map<String, dynamic> map = {'message': '我从Flutter页面回来了'}; String result = await method.invokeMethod('goBackWithResult', map); }), ),
  • Android端依然是通过判断methodCall.method的值来执行指定的代码,通过methodCall.argument()获取Flutter传递的参数。nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(@NonNull MethodCall methodCall, @NonNull MethodChannel.Result result) { if ("goBackWithResult".equals(methodCall.method)) { // 返回上一页,携带数据 Intent backIntent = new Intent(); backIntent.putExtra("message", (String) methodCall.argument("message")); setResult(RESULT_OK, backIntent); finish(); } } });
14.2 Android原生页面返回Flutter页面
  • Android原生页面返回Flutter页面
    • 这种情况需要原生来调用Flutter代码,和Flutter调用原生方法的步骤是一样的。首先触发flutter页面按钮,从flutter跳转na页面,然后触发na页面返回操作,返回到Flutter页面,并传递数据。
  • 首先是flutter页面触发跳转到na页面的代码操作逻辑,代码如下所示//flutter new Padding( padding: const EdgeInsets.only(left: 10.0, top: 10.0, right: 10.0), child: new RaisedButton( textColor: Colors.black, child: new Text('跳转到原生逗比界面,回调结果:$_methodResult1'), onPressed: () { _jumpToNative(); }), ),
代码语言:txt
复制
//na,注意na接收到flutter指令后,na是调用startActivityForResult操作跳转到na的新页面
代码语言:txt
复制
nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
代码语言:txt
复制
    @Override
代码语言:txt
复制
    public void onMethodCall(@NonNull MethodCall methodCall, @NonNull MethodChannel.Result result) {
代码语言:txt
复制
        if ("doubi".equals(methodCall.method)) {
代码语言:txt
复制
            //接收来自flutter的指令
代码语言:txt
复制
            //跳转到指定Activity
代码语言:txt
复制
            Intent intent = new Intent(MethodChannelActivity.this, MethodResultActivity.class);
代码语言:txt
复制
            startActivityForResult(intent,RESULT_OK2);
代码语言:txt
复制
            //返回给flutter的参数
代码语言:txt
复制
            result.success("Na收到指令");
代码语言:txt
复制
        }
代码语言:txt
复制
    }
代码语言:txt
复制
});
代码语言:txt
复制
```
  • 然后接下来的一步是,从NA返回到flutter页面,然后再去调用flutter方法。具体操作代码如下所示//na flutter触发打开na的新的页面 public class MethodResultActivity extends AppCompatActivity {
代码语言:txt
复制
    @SuppressLint("SetTextI18n")
代码语言:txt
复制
    @Override
代码语言:txt
复制
    protected void onCreate(@Nullable Bundle savedInstanceState) {
代码语言:txt
复制
        super.onCreate(savedInstanceState);
代码语言:txt
复制
        setContentView(R.layout.activity_android);
代码语言:txt
复制
        TextView tv = findViewById(R.id.tv);
代码语言:txt
复制
        tv.setText("flutter页面打开NA页面,测试Android原生页面返回Flutter页面");
代码语言:txt
复制
        tv.setOnClickListener(new View.OnClickListener() {
代码语言:txt
复制
            @Override
代码语言:txt
复制
            public void onClick(View view) {
代码语言:txt
复制
                Intent intent = new Intent();
代码语言:txt
复制
                intent.putExtra("message", "我从原生页面回来了");
代码语言:txt
复制
                setResult(RESULT_OK2, intent);
代码语言:txt
复制
                finish();
代码语言:txt
复制
            }
代码语言:txt
复制
        });
代码语言:txt
复制
    }
代码语言:txt
复制
}
代码语言:txt
复制
// na flutter承载容器的na的原生页面
代码语言:txt
复制
@Override
代码语言:txt
复制
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
代码语言:txt
复制
    super.onActivityResult(requestCode, resultCode, data);
代码语言:txt
复制
    if (data != null && resultCode==RESULT_OK2) {
代码语言:txt
复制
        // MethodResultActivity返回的数据
代码语言:txt
复制
        String message = data.getStringExtra("message");
代码语言:txt
复制
        Map<String, Object> result = new HashMap<>();
代码语言:txt
复制
        result.put("message", message);
代码语言:txt
复制
        // 调用Flutter端定义的方法
代码语言:txt
复制
        nativeChannel.invokeMethod("onActivityResult", result, new MethodChannel.Result() {
代码语言:txt
复制
            @SuppressLint("SetTextI18n")
代码语言:txt
复制
            @Override
代码语言:txt
复制
            public void success(@Nullable Object result) {
代码语言:txt
复制
                tvContent.setText("测试内容2:"+result);
代码语言:txt
复制
            }
代码语言:txt
复制
            @SuppressLint("SetTextI18n")
代码语言:txt
复制
            @Override
代码语言:txt
复制
            public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
代码语言:txt
复制
                tvContent.setText("测试内容:flutter传递给na数据传递错误2");
代码语言:txt
复制
            }
代码语言:txt
复制
            @Override
代码语言:txt
复制
            public void notImplemented() {
代码语言:txt
复制
            }
代码语言:txt
复制
        });
代码语言:txt
复制
    }
代码语言:txt
复制
}
代码语言:txt
复制
//flutter
代码语言:txt
复制
  Future<dynamic> handler(MethodCall call) async {
代码语言:txt
复制
    switch (call.method) {
代码语言:txt
复制
      case 'onActivityResult':
代码语言:txt
复制
        // 获取原生页面传递的参数
代码语言:txt
复制
        print(call.arguments['message']);
代码语言:txt
复制
        return "你好,这个是从flutter传递过来的数据";
代码语言:txt
复制
    }
代码语言:txt
复制
  }
代码语言:txt
复制
  flutterChannel.setMethodCallHandler(handler);
代码语言:txt
复制
```

推荐

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录介绍
  • 推荐
  • 01.flutter和原生之间交互
    • 1.1 交互简单介绍
      • 1.2 核心类重点说明
      • 02.MethodChannel流程
      • 03.MethodChanel使用流程
        • 3.1 flutter调用native
          • 3.2 native调用flutter
          • 04.MethodChanel代码实践
            • 4.1 native调用flutter
              • 4.2 flutter调用native
              • 05.EventChannel流程
              • 06.EventChannel基本流程
              • 07.EventChannel代码实现
              • 08.BasicMessageChannel流程
              • 09.BasicMessageChannel基本流程
              • 10.BasicMessageChannel代码实现
                • 10.1flutter端
                  • 10.2 native(android)端
                  • 11.Channel编解码器说明
                    • 11.1 什么是消息编解码器
                      • 11.2 四种消息编解码器类型
                        • 11.3 编码器的源码分析下
                          • 11.4 看StandardMessageCodec
                            • 11.5 如何选择合适编解码器
                            • 12.Channel通信可以子线程吗
                              • 12.1 Android发送通信信息
                                • 12.Flutter给NA发送数据
                                • 13.Channel通信传递稳定性
                                  • 13.1 Android给flutter发送数据
                                    • 13.2 查看数据稳定性和及时性
                                    • 14.onActivityResult如何实现
                                      • 14.1 Flutter页面返回Android原生页面
                                        • 14.2 Android原生页面返回Flutter页面
                                        • 推荐
                                        相关产品与服务
                                        容器服务
                                        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                        领券
                                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档