Flutter Platform Channel 使用与源码分析

2019-08-2119:16:51APP与小程序开发Comments3,220 views字数 14731阅读模式

1. 为什么要有 PlatformChannel

1、如果 Flutter 要获取设备的电量信息怎么办?
2、如果 Flutter 要实时监控网络状态怎么办?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

由于 Flutter 特点如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

1、Flutter 是一个跨平台的 UI 库,专注于构建高效的 UI。
2、多平台的支持,下图是 Flutter 目前支持的平台,每个平台的都有自己的平台特性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

Flutter Platform Channel 使用与源码分析文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

基于以上两点,目前 Flutter 如果要和平台相关的部分通信需要有一个通道即 PlatformChannel。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

2. 架构图

Flutter Platform Channel 使用与源码分析文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

3. PlatformChannel 类型

  • BasicMessageChannel:用于数据传递。platform 和 dart 可互相传递数据(asynchronous message passing)
  • MethodChannel:用于传递方法调用。platform 和 dart 可互相调用方法(asynchronous method calls)
  • EventChannel:用于数据流通信。建立连接之后,platform 发送消息,dart 接收消息(event streams)

三种类型的 channel 都定义在 platform_channel.dart 中,从源码中可以看到三种 channel 都用到了以下三个属性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

  • name:String 类型,表示 channel 的名字,全局唯一(The logical channel on which communication happens)
  • codec:MessageCodec 类型,消息的编码解码器(The message codec used by this channel)
  • binaryMessenger:BinaryMessenger类型,用于发送数据(The messenger used by this channel to send platform messages)

3.1 channel name

channel 的名字,每个 Flutter 应用可能有多个 channel,但是每个 channel 必须有一个唯一的名字。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

3.2 codec

codec 用来对数据编码解码,以便两端可以正确读取数据。 Flutter Platform Channel 使用与源码分析" data-data-original="https://user-gold-cdn.xitu.io/2019/8/21/16cb1e0768173ead?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" src="https://www.cainiaoxueyuan.com/wp-content/themes/begin/img/loading.png"height="20" data-width="1280" data-height="430" />文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

3.3 binaryMessenger

用于发送数据文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

4. PlatformChannel 使用

4.1 MethodChannel

  • Dart 调用 Android 方法

method_channel_page.dart 主要代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

第一步
static const methodChannel = MethodChannel("method_channel_sample");

第二步  
Future<dynamic> getUserInfo(String method, {String userName}) async {
  return await methodChannel.invokeMethod(method, userName);
}

第三步    
MaterialButton(
  color: Colors.blue,
  textColor: Colors.white,
  child: new Text('获取 snow 用户信息'),
  onPressed: () {
    getUserInfo("getInfo", userName: "snow")
      ..then((result) {
        setState(() {
          messageFromNative = result;
        });
      });
  },
),
复制代码

MainActivity.java 主要代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

private void addMethodChannel() {
    mMethodChannel = new MethodChannel(getFlutterView(), "method_channel_sample");
    mMethodChannel.setMethodCallHandler((methodCall, result) -> {

        String method = methodCall.method;

        if ("getInfo".equals(method)) {

            String userName = (String) methodCall.arguments;

            if (userName.equals("rocx")) {
                String user = "name:rocx, age:18";
                result.success(user);
            } else {
                result.success("user not found");

                invokeSayHelloMethod();
            }
        }

    });
}
复制代码

从以上代码可以看出
Dart 调用 Android 代码分三步。首先在 Dart 端定义 MethodChannel 名字为 method_channel_sample。然后定义getUserInfo方法,传入要调用的方法名和参数。最后点击按钮执行方法,获取用户信息。
在 Android 端定一个 MethodChannel 名字和 Dart 端保持一致。设置 MethodCallHandler。当调用的是getInfo方法时,根据参数返回信息。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

  • Android 调用 Dart 方法

MainActivity.java 主要代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

private void invokeSayHelloMethod() {
    mMethodChannel.invokeMethod("sayHello", "", new MethodChannel.Result() {
        @Override
        public void success(Object o) {

            Toast.makeText(MainActivity.this, o.toString(), Toast.LENGTH_LONG).show();
        }

        @Override
        public void error(String s, String s1, Object o) {

        }

        @Override
        public void notImplemented() {

        }
    });
}
复制代码

method_channel_page.dart 主要代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

Future<dynamic> addHandler(MethodCall call) async {
  switch (call.method) {
    case "sayHello":
      return "Hello from Flutter";
      break;
  }
}

@override
void initState() {
  super.initState();

  methodChannel.setMethodCallHandler(addHandler);
}
复制代码

从代码可以看出,在 Dart 端设置 MethodCallHandler 然后在 Android 端调用即可。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

4.2 BasicMessageChannel

  • Dart 向 Android 发送消息

basic_message_channel_page.dart 主要代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

第一步
static const basicMessageChannel = BasicMessageChannel(
      "basic_message_channel_sample", StandardMessageCodec());

第二步
Future<dynamic> sayHelloToNative(String message) async {
  String reply = await basicMessageChannel.send(message);

  setState(() {
    msgReplyFromNative = reply;
  });

  return reply;
}

第三步
MaterialButton(
  color: Colors.blue,
  textColor: Colors.white,
  child: new Text('say hello to native'),
  onPressed: () {
    sayHelloToNative("hello");
  },
),

复制代码

MainActivity.java 主要代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

private void addBasicMessageChannel() {
    mBasicMessageChannel = new BasicMessageChannel<>(getFlutterView(), "basic_message_channel_sample", StandardMessageCodec.INSTANCE);
    mBasicMessageChannel.setMessageHandler((object, reply) -> {

        reply.reply("receive " + object.toString() + " from flutter");

        mBasicMessageChannel.send("native say hello to flutter");
    });
}

复制代码

从以上代码可以看出
Dart 向 Android 发送消息依然分为三步。首先在 Dart 端定义 BasicMessageChannel 名字为 basic_message_channel_sample。然后定义发送消息的方法sayHelloToNative。最后点击按钮向 Android 端发送消息。
在 Android 端定一个 BasicMessageChannel 名字和 Dart 端保持一致。设置 MethodCallHandler。当收到消息时发一个回复。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

  • Android 向 Dart 发送消息

MainActivity.java 主要代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

mBasicMessageChannel.send("native say hello to flutter");
复制代码

basic_message_channel_page.dart 主要代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

Future<dynamic> addHandler(Object result) async {
  setState(() {
    msgReceiveFromNative = result.toString();
  });
}

void addMessageListener() {
  basicMessageChannel.setMessageHandler(addHandler);
}

@override
void initState() {
  super.initState();
  addMessageListener();
}

复制代码

从代码可以看出,在 Dart 端设置 MessageHandler 然后在 Android 端直接发送消息即可。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

4.3 EventChannel

event_channel_page.dart 主要代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

第一步
static const eventChannel = EventChannel("event_channel_sample");

void _onEvent(Object event) {
  setState(() {
    if (_streamSubscription != null) {
      eventMessage = event.toString();
    }
  });
}

void _onError(Object error) {
  setState(() {
    if (_streamSubscription != null) {
      eventMessage = "error";
    }
  });
}

@override
void initState() {
  super.initState();
  eventMessage = "";
  第二步
  _streamSubscription = eventChannel
      .receiveBroadcastStream()
      .listen(_onEvent, onError: _onError);
}

复制代码

MainActivity.java 主要代码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

private void addEventChannel() {
    mEventChannel = new EventChannel(getFlutterView(), "event_channel_sample");
    mEventChannel.setStreamHandler(new EventChannel.StreamHandler() {
        @Override
        public void onListen(Object o, EventChannel.EventSink eventSink) {

            task = new TimerTask() {
                @Override
                public void run() {
                    runOnUiThread(() -> eventSink.success("i miss you " + System.currentTimeMillis()));

                }
            };
            timer = new Timer();
            timer.schedule(task, 2000, 3000);
        }

        @Override
        public void onCancel(Object o) {
            task.cancel();
            timer.cancel();
            task = null;
            timer = null;
        }
    });
}
复制代码

Dart 接受 Android stream event。首先在 Dart 端定义 EventChannel 名字为 event_channel_sample。然后设置receiveBroadcastStream监听,当 Android 端有消息发过来会回调_onEvent方法。
在 Android 端启动一个定时器,每隔3s向 Dart 端发送一次消息。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

4.4 总结

如下图,在 Dart 与 Platform 通信过程中,通过 channel name 找到对方,然后把消息通过 codec 进行编解码,最后通过 binaryMessenger 进行发送。 Flutter Platform Channel 使用与源码分析文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5. 源码分析-以 MethodChannel 为例

5.1 调用 MethodChannel 的 invokeMethod 方法,会调用到 binaryMessenger.send 方法。即 binaryMessenger.send 传入 channel name 和编码好的参数。
  @optionalTypeArgs
  Future<T> invokeMethod<T>(String method, [ dynamic arguments ]) async {
    assert(method != null);
    final ByteData result = await binaryMessenger.send(
      name,
      codec.encodeMethodCall(MethodCall(method, arguments)),
    );
    if (result == null) {
      throw MissingPluginException('No implementation found for method $method on channel $name');
    }
    final T typedResult = codec.decodeEnvelope(result);
    return typedResult;
  }
复制代码
5.2 binary_messenger.dart 的 send 方法会调用当前对象的 _sendPlatformMessage 方法,最终会调用 window.sendPlatformMessage 方法。
  @override
  Future<ByteData> send(String channel, ByteData message) {
    final MessageHandler handler = _mockHandlers[channel];
    if (handler != null)
      return handler(message);
    return _sendPlatformMessage(channel, message);
  }
复制代码
  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;
  }
复制代码
5.3 在 window.dart 中又调用了 native 方法 _sendPlatformMessage。
  void sendPlatformMessage(String name,
                           ByteData data,
                           PlatformMessageResponseCallback callback) {
    final String error =
        _sendPlatformMessage(name, _zonedPlatformMessageResponseCallback(callback), data);
    if (error != null)
      throw Exception(error);
  }
复制代码
  String _sendPlatformMessage(String name,
                              PlatformMessageResponseCallback callback,
                              ByteData data) native 'Window_sendPlatformMessage';
复制代码
5.4 接下来进入 engine 中的 window.cc,可以看到最终调用的是 dart_state->window()->client()->HandlePlatformMessage。
void Window::RegisterNatives(tonic::DartLibraryNatives* natives) {
  natives->Register({
      {"Window_defaultRouteName", DefaultRouteName, 1, true},
      {"Window_scheduleFrame", ScheduleFrame, 1, true},
      {"Window_sendPlatformMessage", _SendPlatformMessage, 4, true},
      {"Window_respondToPlatformMessage", _RespondToPlatformMessage, 3, true},
      {"Window_render", Render, 2, true},
      {"Window_updateSemantics", UpdateSemantics, 2, true},
      {"Window_setIsolateDebugName", SetIsolateDebugName, 2, true},
      {"Window_reportUnhandledException", ReportUnhandledException, 2, true},
      {"Window_setNeedsReportTimings", SetNeedsReportTimings, 2, true},
  });
}
复制代码
void _SendPlatformMessage(Dart_NativeArguments args) {
  tonic::DartCallStatic(&SendPlatformMessage, args);
}
复制代码
Dart_Handle SendPlatformMessage(Dart_Handle window,
                                const std::string& name,
                                Dart_Handle callback,
                                Dart_Handle data_handle) {
  UIDartState* dart_state = UIDartState::Current();

  if (!dart_state->window()) {
    return tonic::ToDart(
        "Platform messages can only be sent from the main isolate");
  }

  fml::RefPtr<PlatformMessageResponse> response;
  if (!Dart_IsNull(callback)) {
    response = fml::MakeRefCounted<PlatformMessageResponseDart>(
        tonic::DartPersistentValue(dart_state, callback),
        dart_state->GetTaskRunners().GetUITaskRunner());
  }
  if (Dart_IsNull(data_handle)) {
    dart_state->window()->client()->HandlePlatformMessage(
        fml::MakeRefCounted<PlatformMessage>(name, response));
  } else {
    tonic::DartByteData data(data_handle);
    const uint8_t* buffer = static_cast<const uint8_t*>(data.data());
    dart_state->window()->client()->HandlePlatformMessage(
        fml::MakeRefCounted<PlatformMessage>(
            name, std::vector<uint8_t>(buffer, buffer + data.length_in_bytes()),
            response));
  }

  return Dart_Null();
}
复制代码

window.cc 源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5.5 我们进入 window.h 中找到 client 其实是 WindowClient。
  WindowClient* client() const { return client_; }
复制代码

window.h 源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5.6 在 runtime_controller.h 中可以看到 RuntimeController 是 WindowClient 的实际实现,调用的是 RuntimeController 的 HandlePlatformMessage 方法。
class RuntimeController final : public WindowClient {

...
  // |WindowClient|
  void HandlePlatformMessage(fml::RefPtr<PlatformMessage> message) override;
...
}
复制代码

runtime_controller.h 源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5.7 在 runtime_controller.cc 中,HandlePlatformMessage 调用了 client_ 的 HandlePlatformMessage 方法,client_ 实际是代理对象 RuntimeDelegate。
void RuntimeController::HandlePlatformMessage(
    fml::RefPtr<PlatformMessage> message) {
  client_.HandlePlatformMessage(std::move(message));
}
复制代码
RuntimeDelegate& p_client
复制代码

runtime_controller.cc 源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5.8 engine.h 是 RuntimeDelegate 的具体实现类。
class Engine final : public RuntimeDelegate {
...
  // |RuntimeDelegate|
  void HandlePlatformMessage(fml::RefPtr<PlatformMessage> message) override;
...  
}  
复制代码

engine.h 源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5.9 engine.cc 中调用了 delegate_ 的 OnEngineHandlePlatformMessage 方法。
void Engine::HandlePlatformMessage(fml::RefPtr<PlatformMessage> message) {
  if (message->channel() == kAssetChannel) {
    HandleAssetPlatformMessage(std::move(message));
  } else {
    delegate_.OnEngineHandlePlatformMessage(std::move(message));
  }
}
复制代码

engine.cc 源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5.10 shell.h 是 Engine 的代理。
  // |Engine::Delegate|
  void OnEngineHandlePlatformMessage(
      fml::RefPtr<PlatformMessage> message) override;
复制代码

shell.h 源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5.11 调用流程又进入了 shell.cc 的 HandleEngineSkiaMessage 方法,把消费放到 TaskRunner 中。
// |Engine::Delegate|
void Shell::OnEngineHandlePlatformMessage(
    fml::RefPtr<PlatformMessage> message) {
  FML_DCHECK(is_setup_);
  FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread());

  if (message->channel() == kSkiaChannel) {
    HandleEngineSkiaMessage(std::move(message));
    return;
  }

  task_runners_.GetPlatformTaskRunner()->PostTask(
      [view = platform_view_->GetWeakPtr(), message = std::move(message)]() {
        if (view) {
          view->HandlePlatformMessage(std::move(message));
        }
      });
}
复制代码

shell.cc 源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5.12 当 task 执行是会调用 platform_view_android.h 的 HandlePlatformMessage 方法。
class PlatformViewAndroid final : public PlatformView {
...
  // |PlatformView|
  void HandlePlatformMessage(
      fml::RefPtr<flutter::PlatformMessage> message) override;
...
}
复制代码

platform_view_android.h 源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5.13 在 platform_view_android.cc 的 HandlePlatformMessage 中,开始通过 jni 调用 java 端的方法,java_channel 即要找的 channel。
// |PlatformView|
void PlatformViewAndroid::HandlePlatformMessage(
    fml::RefPtr<flutter::PlatformMessage> message) {
  JNIEnv* env = fml::jni::AttachCurrentThread();
  fml::jni::ScopedJavaLocalRef<jobject> view = java_object_.get(env);
  if (view.is_null())
    return;

  int response_id = 0;
  if (auto response = message->response()) {
    response_id = next_response_id_++;
    pending_responses_[response_id] = response;
  }
  auto java_channel = fml::jni::StringToJavaString(env, message->channel());
  if (message->hasData()) {
    fml::jni::ScopedJavaLocalRef<jbyteArray> message_array(
        env, env->NewByteArray(message->data().size()));
    env->SetByteArrayRegion(
        message_array.obj(), 0, message->data().size(),
        reinterpret_cast<const jbyte*>(message->data().data()));
    message = nullptr;

    // This call can re-enter in InvokePlatformMessageXxxResponseCallback.
    FlutterViewHandlePlatformMessage(env, view.obj(), java_channel.obj(),
                                     message_array.obj(), response_id);
  } else {
    message = nullptr;

    // This call can re-enter in InvokePlatformMessageXxxResponseCallback.
    FlutterViewHandlePlatformMessage(env, view.obj(), java_channel.obj(),
                                     nullptr, response_id);
  }
}
复制代码

platform_view_android.cc 源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5.14 在 platform_view_android_jni.cc 中可以看到 g_handle_platform_message_method 就是 FindClass("io/flutter/embedding/engine/FlutterJNI") 类的 handlePlatformMessage 方法。至此 engine 代码执行结束。
static jmethodID g_handle_platform_message_method = nullptr;
void FlutterViewHandlePlatformMessage(JNIEnv* env,
                                      jobject obj,
                                      jstring channel,
                                      jobject message,
                                      jint responseId) {
  env->CallVoidMethod(obj, g_handle_platform_message_method, channel, message,
                      responseId);
  FML_CHECK(CheckException(env));
}
复制代码
  g_handle_platform_message_method =
      env->GetMethodID(g_flutter_jni_class->obj(), "handlePlatformMessage",
                       "(Ljava/lang/String;[BI)V");
复制代码
  g_flutter_jni_class = new fml::jni::ScopedJavaGlobalRef<jclass>(
      env, env->FindClass("io/flutter/embedding/engine/FlutterJNI"));
  if (g_flutter_jni_class->is_null()) {
    FML_LOG(ERROR) << "Failed to find FlutterJNI Class.";
    return false;
  }
复制代码

platform_view_android_jni.cc 源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

5.15 在 FlutterJNI 中调用了 this.platformMessageHandler.handleMessageFromDart 方法。也就是 DartMessenger 的 handleMessageFromDart 方法。
    private void handlePlatformMessage(@NonNull String channel, byte[] message, int replyId) {
        if (this.platformMessageHandler != null) {
            this.platformMessageHandler.handleMessageFromDart(channel, message, replyId);
        }

    }
复制代码
5.16 DartMessenger 中 messageHandlers 通过 channel 名找到对应的 handler 进行处理,这个 handler 就是我们在 java 代码里通过 channel 设置的,整个调用流程完成。
    public void handleMessageFromDart(@NonNull String channel, @Nullable byte[] message, int replyId) {
        Log.v("DartMessenger", "Received message from Dart over channel '" + channel + "'");
        BinaryMessageHandler handler = (BinaryMessageHandler)this.messageHandlers.get(channel);
        if (handler != null) {
            try {
                Log.v("DartMessenger", "Deferring to registered handler to process message.");
                ByteBuffer buffer = message == null ? null : ByteBuffer.wrap(message);
                handler.onMessage(buffer, new DartMessenger.Reply(this.flutterJNI, replyId));
            } catch (Exception var6) {
                Log.e("DartMessenger", "Uncaught exception in binary message listener", var6);
                this.flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
            }
        } else {
            Log.v("DartMessenger", "No registered handler for message. Responding to Dart with empty reply message.");
            this.flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
        }

    }

复制代码

Demo 地址

flutter_platform_channel 使用文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

作者:奇舞移动
链接:https://juejin.im/post/5d5ca3f8e51d456216553504
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15721.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/xcx/15721.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定