Flutter MethodChannel的使用及原理解析

Dart和原生通信

一次典型的方法调用过程类似网络调用,由作为客户端的 Flutter,通过方法通道向作为服务端的原生代码宿主发送方法调用请求,原生代码宿主在监听到方法调用的消息后,调用平台相关的 API 来处理 Flutter 发起的请求,最后将处理完毕的结果通过方法通道回发至 Flutter。调用过程如下图所示:

image

1. 基本用法

以跳转应用市场为例

//声明MethodChannel
const platform = MethodChannel('channelName');

//处理按钮点击
handleButtonClick() async{
  int result;
  //异常捕获
  try {
    //异步等待方法通道的调用结果
    result = await platform.invokeMethod('openAppMarket');
  }
  catch (e) {
    result = -1;
  }
  print("Result:$result");
}

1.1 Android端实现
MainActivity.java

public class MainActivity extends FlutterActivity {
    MethodChannel methodChannel;

    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        super.configureFlutterEngine(flutterEngine);
        methodChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "channelName");
        methodChannel.setMethodCallHandler((call, result) -> {
            //判断方法名是否支持
            if(call.method.equals("openAppMarket")) {
                try {
                    //应用市场URI
                    Uri uri = Uri.parse("market://details?id=com.hangchen.example.flutter_module_page.host");
                    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    //打开应用市场
                    startActivity(intent);
                    //返回处理结果
                    result.success(0);
                } catch (Exception e) {
                    //打开应用市场出现异常
                    result.error("UNAVAILABLE", "没有安装应用市场", null);
                }
            }else {
                //方法名暂不支持
                result.notImplemented();
            }
        });
    }
}

1.2 iOS端实现
AppleDelegate.m


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  //创建命名方法通道
  FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"samples.chenhang/utils" binaryMessenger:(FlutterViewController *)self.window.rootViewController];
  //往方法通道注册方法调用处理回调
  [channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
    //方法名称一致
    if ([@"openAppMarket" isEqualToString:call.method]) {
      //打开App Store(本例打开微信的URL)
      [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"itms-apps://itunes.apple.com/xy/app/foo/id414478124"]];
      //返回方法处理结果
      result(@0);
    } else {
      //找不到被调用的方法
      result(FlutterMethodNotImplemented);
    }
  }];
  ...
}

2. 原理分析

Dart这边的MethodChannel类,有几个重要的成员变量:

  /// 方法通道的名字 not null
  final String name;

  /// 信息的编解码器 not null
  final MethodCodec codec;

  /// 用来发送消息的messenger
  /// 未设定的话就用默认的
  BinaryMessenger get binaryMessenger => _binaryMessenger ?? defaultBinaryMessenger;
  final BinaryMessenger? _binaryMessenger;

2.1 调用invokeMethod来获取通信结果

Future<T?> _invokeMethod<T>(String method, { required bool missingOk, dynamic arguments }) async {
    final ByteData? result = await binaryMessenger.send(
      name,
      codec.encodeMethodCall(MethodCall(method, arguments)),
    );
    if (result == null) {
      if (missingOk) {
        return null;
      }
      throw MissingPluginException('No implementation found for method $method on channel $name');
    }
    return codec.decodeEnvelope(result) as T?;
  }

先是将我们要调用的方法名构造了一个MethodCall,然后用codec编码,之后通过binaryMessenger发送了一条消息,调用结果会被封装成一个ByteData,然后用codec解码。

2.2 StandartMethodCodec
codec默认是一个StandardMethodCodec,看下它的encodeMethodCall方法做了什么

@override
ByteData encodeMethodCall(MethodCall call) {
    final WriteBuffer buffer = WriteBuffer();
    messageCodec.writeValue(buffer, call.method);
    messageCodec.writeValue(buffer, call.arguments);
    return buffer.done();
}

构造了一个WriteBuffer,然后将方法名和buffer传进messageCodec

void writeValue(WriteBuffer buffer, dynamic value) {
    ...
    //会走到这
    else if (value is String) {
        //先写进一个标识为String的int值,其它类型有不同的值
      buffer.putUint8(_valueString);
      //转换为Uinnt8List,就是一个二进制数组
      final Uint8List bytes = utf8.encoder.convert(value);
      //将非负32位整数[值]写入[缓冲区]使用扩展的1-5字节编码来优化小值。
      writeSize(buffer, bytes.length);
      //将二进制数组写入buffer
      buffer.putUint8List(bytes);
    }
} 

2.3 WriteBuffer
调用buffer.done()方法,将ByteBuffer转换为ByteData。

ByteData done() {
    final ByteData result = _buffer!.buffer.asByteData(0, _buffer!.lengthInBytes);
    _buffer = null;
    return result;
}

2.4 Send
回到[2.1]invokeMethod方法,看下binaryMessenger的send方法做了什么,这个binaryMessenger实际上是binding.dart里面的一个类_DefaultBinaryMessenger,继承了BinaryMessager,它的send方法如下:

@override
Future<ByteData?>? send(String channel, ByteData? message) {
    //先判断有没有设置mockHandler,有的话就用mockhandler处理信息,这种情况一般用于单元测试
    final MessageHandler? handler = _mockHandlers[channel];
    if (handler != null)
        return handler(message);
    return _sendPlatformMessage(channel, message);
}

调用_sendPlatformMessage()

Future<ByteData?> _sendPlatformMessage(String channel, ByteData? message) {
    final Completer<ByteData?> completer = Completer<ByteData?>();
    //调用PatformDispatcher的sendPlatformMessage方法
    ui.PlatformDispatcher.instance.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;
  }

最后会调用到PlatformDispatcher的_sendPlatformMessage方法,进入flutter engine层的_SendPlatformMessage。

3. Engine层

3.1 PlatformConfiguration
//platform_configuration.cc

void _SendPlatformMessage(Dart_NativeArguments args) {
  tonic::DartCallStatic(&SendPlatformMessage, args);
}
}
void PlatformConfiguration::RegisterNatives(
    tonic::DartLibraryNatives* natives) {
  natives->Register({
      {"PlatformConfiguration_sendPlatformMessage", _SendPlatformMessage, 4,
       true},
      ...
  });
}

PlatformDispatcher中的_sendPlatformMessage最终对应于引擎层中platform_configuration.cc的_SendPlatformMessage()方法,继续调用SendPlatformMessage()。

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->platform_configuration()) {
    return tonic::ToDart(
        "Platform messages can only be sent from the main isolate");
  }

  fml::RefPtr<PlatformMessageResponse> response;
  if (!Dart_IsNull(callback)) {
    //PlatformMessageResponseDart对象中采用的是UITaskRunner
    response = fml::MakeRefCounted<PlatformMessageResponseDart>(
        tonic::DartPersistentValue(dart_state, callback),
        dart_state->GetTaskRunners().GetUITaskRunner());
  }
  if (Dart_IsNull(data_handle)) {
    dart_state->platform_configuration()->client()->HandlePlatformMessage(
        std::make_unique<PlatformMessage>(name, response));
  } else {
    tonic::DartByteData data(data_handle);
    const uint8_t* buffer = static_cast<const uint8_t*>(data.data());
    dart_state->platform_configuration()->client()->HandlePlatformMessage(
        std::make_unique<PlatformMessage>(
            name, fml::MallocMapping::Copy(buffer, data.length_in_bytes()),
            response));
  }

  return Dart_Null();
}

name: channel名
data_handle: 待执行的方法名和参数
callback: 执行后回调反馈结果数据的方法
这里还创建了一个PlatformMessageResponseDart对象,保存callback方法。
最后调用RuntimeController(继承了PlatformConfigurationClient)的HandlePlatformMessage来处理平台消息。

3.2 RuntimeController
//flutter/runtime/runtime_controller.cc

void RuntimeController::HandlePlatformMessage(
    std::unique_ptr<PlatformMessage> message) {
  client_.HandlePlatformMessage(std::move(message));
}

Flutter engine的调用关系如下:
engin

上面的client来自engine,所以走到engine的HandlePlatformMessage方法。

3.3 Engine
//flutter/shell/common/engine.cc

static constexpr char kAssetChannel[] = "flutter/assets";

void Engine::HandlePlatformMessage(fml::RefPtr<PlatformMessage> message) {
  if (message->channel() == kAssetChannel) {
    HandleAssetPlatformMessage(std::move(message));
  } else {
    delegate_.OnEngineHandlePlatformMessage(std::move(message));
  }
}

这里的delegate_来自shell,走到shell的OnEngineHandlePlatformMessage。

3.4 Shell
//flutter/shell/common/shell.cc

constexpr char kSkiaChannel[] = "flutter/skia";

void Shell::OnEngineHandlePlatformMessage(
    fml::RefPtr<PlatformMessage> message) {
  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));
        }
      });
}

接下来将HandlePlatformMessage的工作交给主线程的PlatformTaskRunner来处理,调用了platform_view的HandlePlatformMessage方法,这个view对于Android平台的实例为PlatformViewAndroid。

3.5 PlatformViewAndroid
//flutter/shell/platform/android/platform_view_android.cc

std::unordered_map<int, fml::RefPtr<flutter::PlatformMessageResponse>>
      pending_responses_;

// |PlatformView|
void PlatformViewAndroid::HandlePlatformMessage(
    std::unique_ptr<flutter::PlatformMessage> message) {
  int response_id = 0;
  if (auto response = message->response()) {
    response_id = next_response_id_++;
    pending_responses_[response_id] = response;
  }
  // This call can re-enter in InvokePlatformMessageXxxResponseCallback.
  jni_facade_->FlutterViewHandlePlatformMessage(std::move(message),
                                                response_id);
  message = nullptr;
}

pending_response_是一个map数据类型,key为response_id(一个计数),value为message的response,这个map里面记录这所有待响应的PlatformMessageResponse数据。

//flutter/shell/platform/android/platform_view_android_jni_impl.cc

void FlutterViewHandlePlatformMessage(JNIEnv* env, jobject obj,
                                      jstring channel, jobject message,
                                      jint responseId) {
  env->CallVoidMethod(obj, g_handle_platform_message_method, channel, message, responseId);
}

这里就回到Java层了,对应FlutterJNI.java中的handlePlatformMessage()方法。

4. 宿主层

先来看看Java层的MethodChannel

public final class MethodChannel {
    private final BinaryMessenger messenger;
    private final String name;
    private final MethodCodec codec;

    public MethodChannel(BinaryMessenger messenger, String name) {
        //创建MethodChannel
        this(messenger, name, StandardMethodCodec.INSTANCE);
    }

    public MethodChannel(BinaryMessenger messenger, String name, MethodCodec codec) {
        this.messenger = messenger;
        this.name = name;
        this.codec = codec;
    }
}

可以发现是跟Dart的MethodChannel一一对应的,其中的BinaryMessenger是一个接口,在Activity重写configFlutterEngine函数的时候通过flutterEngine.getDartExecutor().getBinaryMessenger()拿到,然后创建MethodChannel。

configFlutterEngine函数定义在FlutterActivityAndFragmentDelegate类的内部接口FlutterActivityAndFragmentDelegate.Host,而FlutterActivity实现了这个接口,并且持有一个FlutterActivityAndFragmentDelegate成员变量delegate,在onCreate中初始化这个delegate。

4.1 FlutterActivity
// FlutterActivity.java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  delegate = new FlutterActivityAndFragmentDelegate(this);
  //在这里会触发flutterEngin的初始化setupFlutterEngine()
  delegate.onAttach(this);
  delegate.onRestoreInstanceState(savedInstanceState);
}

4.2 FlutterActivityAndFragmentDelegate
// FlutterActivityAndFragmentDelegate.java

void setupFlutterEngine() {
    // 先检查是否需要从缓存中获取
    String cachedEngineId = host.getCachedEngineId();
    if (cachedEngineId != null) {
      flutterEngine = FlutterEngineCache.getInstance().get(cachedEngineId);
      isFlutterEngineFromHost = true;
      if (flutterEngine == null) {
        throw new IllegalStateException(
            "The requested cached FlutterEngine did not exist in the FlutterEngineCache: '"
                + cachedEngineId
                + "'");
      }
      return;
    }

    // 其次把创建FlutterEngine的任务委托给子类,这里的Host即为FlutterActivity
    flutterEngine = host.provideFlutterEngine(host.getContext());
    if (flutterEngine != null) {
      isFlutterEngineFromHost = true;
      return;
    }

    // FlutterActivity不创建,则系统创建
    flutterEngine =
        new FlutterEngine(
            host.getContext(),
            host.getFlutterShellArgs().toArray(),
            /*automaticallyRegisterPlugins=*/ false,
            /*willProvideRestorationData=*/ host.shouldRestoreAndSaveState());
    isFlutterEngineFromHost = false;
  }

FlutterEngine的构造函数中会初始化dartExecutor,DartExecutor的构造函数中会初始化一个DefaultBinaryMessenger。

回到MethodChannel的创建部分,我们调用了它的setMethodCallHandler方法。

4.3 MethodChannel
// io/flutter/plugin/common/MethodChannel.java

public void setMethodCallHandler(final @Nullable MethodCallHandler handler) {
  messenger.setMessageHandler(
      name, handler == null ? null : new IncomingMethodCallHandler(handler));
}

调用了messeger的setMethodCallHandler方法。

4.4 DefaultBinaryMessenger
// DefaultBinaryMessenger

public void setMessageHandler(
    @NonNull String channel, @Nullable BinaryMessenger.BinaryMessageHandler handler) {
  messenger.setMessageHandler(channel, handler);
}

这里的messenger是一个DartMessenger,在创建DefaultBinaryMessenger的时候传进来的。

4.5 DartMessenger

@NonNull private final Map<String, BinaryMessenger.BinaryMessageHandler> messageHandlers;

@Override
public void setMessageHandler(
    @NonNull String channel, @Nullable BinaryMessenger.BinaryMessageHandler handler) {
  if (handler == null) {
    messageHandlers.remove(channel);
  } else {
    messageHandlers.put(channel, handler);
  }
}

将传进来的BinaryMessageHandler保存进了一个map,跟dart层一样,messageHandlers记录着每一个channel所对应的handler方法,此处的handler为IncomingMethodCallHandler。

private final class IncomingMethodCallHandler implements BinaryMessageHandler {
  private final MethodCallHandler handler;
  //保存了我们写的MethodCallHandler
  IncomingMethodCallHandler(MethodCallHandler handler) {
      this.handler = handler;
  }
}

5. Java层

再回到[3.5]FlutterViewHandlePlatformMessage方法,经过JNI将调用FlutterJNI.handlePlatformMessage()方法。

5.1 FlutterJNI
// flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java

private void handlePlatformMessage(final String channel, byte[] message, final int replyId) {
  //这里的replyId即为之前传入的response_id[3.5]
  if (platformMessageHandler != null) {
    platformMessageHandler.handleMessageFromDart(channel, message, replyId);
  }
}

FlutterEngine初始化过程,会创建DartExecutor,调用其onAttachedToJNI()方法来设置platformMessageHandler,其值等于DartMessenger。

public void onAttachedToJNI() {
  //Attached to JNI. Registering the platform message handler for this Dart execution context.
  flutterJNI.setPlatformMessageHandler(dartMessenger);
}

5.2 DartMessenger
看下DartMessenger的handleMessageFromDart方法。
// io/flutter/embedding/engine/dart/DartMessenger.java

public void handleMessageFromDart(final String channel,
                        byte[] message, final int replyId) {
  //从messageHandlers中获取handler
  BinaryMessenger.BinaryMessageHandler handler = messageHandlers.get(channel);
  if (handler != null) {
    try {
      final ByteBuffer buffer = (message == null ? null : ByteBuffer.wrap(message));
      //之前保存的IncomingMethodCallHandler 
      handler.onMessage(buffer, new Reply(flutterJNI, replyId));
    } catch (Exception ex) {
      ...
    }
  } else {
    ...
  }
}

这里的Reply是DartMessenger中的一个静态内部类,继承了BinaryMessager.BinaryReply。

class DartMessenger implements BinaryMessenger, PlatformMessageHandler {

  private static class Reply implements BinaryMessenger.BinaryReply {
    private final FlutterJNI flutterJNI;
    private final int replyId;
    private final AtomicBoolean done = new AtomicBoolean(false);

    Reply(@NonNull FlutterJNI flutterJNI, int replyId) {
      this.flutterJNI = flutterJNI;
      this.replyId = replyId;
    }
  }
}

5.3 IncomingMethodCallHandler
来到IncomingMethodCallHandler的onMessage方法。

@Override
@UiThread
public void onMessage(ByteBuffer message, final BinaryReply reply) {
  //从消息中解码出MethodCall
  final MethodCall call = codec.decodeMethodCall(message);
  try {
    //这里回调了我们自己写的代码,见MainActivity.java
    handler.onMethodCall(
        call,
        new Result() {
          @Override
          public void success(Object result) {
            // 给dart层发送回复
            reply.reply(codec.encodeSuccessEnvelope(result));
          }

          @Override
          public void error(String errorCode, String errorMessage, Object errorDetails) {
            reply.reply(codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails));
          }

          @Override
          public void notImplemented() {
            reply.reply(null);
          }
        });
  } catch (RuntimeException e) {
    reply.reply(
        codec.encodeErrorEnvelopeWithStacktrace(
            "error", e.getMessage(), null, getStackTrace(e)));
  }
}

6. 给Dart层回传结果

// DartMessenger/Reply

public void reply(ByteBuffer reply) {
  if (reply == null) {
    flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
  } else {
    //走这里
    flutterJNI.invokePlatformMessageResponseCallback(replyId, reply, reply.position());
  }
}

6.1 FlutterJNI
进入FlutterJNI

public void invokePlatformMessageResponseCallback(int responseId, ByteBuffer message, int position) {
  if (isAttached()) {
    nativeInvokePlatformMessageResponseCallback(
          nativePlatformViewId, responseId, message, position);
  }
}

到这里message就进入native层了。

6.2 InvokePlatformMessageResponseCallback
// flutter/engine/blob/master/shell/platform/android/platform_view_android_jni_impl.cc

static void InvokePlatformMessageResponseCallback(JNIEnv* env,
                                                  jobject jcaller,
                                                  jlong shell_holder,
                                                  jint responseId,
                                                  jobject message,
                                                  jint position) {
  ANDROID_SHELL_HOLDER->GetPlatformView()
      ->InvokePlatformMessageResponseCallback(env,         //
                                              responseId,  //
                                              message,     //
                                              position     //
      );
}

这里的platformView是PlatformViewAndroid。

6.3 PlatformViewAndroid
// flutter/shell/platform/android/platform_view_android.cc

void PlatformViewAndroid::InvokePlatformMessageResponseCallback(
        JNIEnv* env, jint response_id,
        jobject java_response_data, jint java_response_position) {
  //从pending_responses_根据response_id来查找PlatformMessageResponse
  auto it = pending_responses_.find(response_id);
  uint8_t* response_data = static_cast<uint8_t*>(env->GetDirectBufferAddress(java_response_data));
  //返回结果数据
  std::vector<uint8_t> response = std::vector<uint8_t>(response_data, response_data + java_response_position);
  auto message_response = std::move(it->second);
  pending_responses_.erase(it);
  //将结果写入到之前保存的PlatformMessageResponseDart
  message_response->Complete(std::make_unique<fml::DataMapping>(std::move(response)));
}

调用了PlatformMessageResponseDart的Complete()方法。

6.4 PlatformMessageResponseDart
// flutter/lib/ui/window/platform_message_response_dart.cc

void PlatformMessageResponseDart::Complete(std::unique_ptr<fml::Mapping> data) {
  is_complete_ = true;
  //post到UI线程来执行
  ui_task_runner_->PostTask(fml::MakeCopyable(
      [callback = std::move(callback_), data = std::move(data)]() mutable {
        std::shared_ptr<tonic::DartState> dart_state = callback.dart_state().lock();
        tonic::DartState::Scope scope(dart_state);
        Dart_Handle byte_buffer = WrapByteData(std::move(data));
        tonic::DartInvoke(callback.Release(), {byte_buffer});
      }));
}

此处将任务Post到UI线程的UITaskRunner来执行。

6.5 DartInvoke
// third_party/tonic/logging/dart_invoke.cc

Dart_Handle DartInvoke(Dart_Handle closure,
                       std::initializer_list<Dart_Handle> args) {
  int argc = args.size();
  Dart_Handle* argv = const_cast<Dart_Handle*>(args.begin());
  Dart_Handle handle = Dart_InvokeClosure(closure, argc, argv);
  return handle;
}

该方法参数closure,也就是PlatformMessageResponseDart中的callback_,不断回溯可知所对应方法便是Dart层BinaryMessages的_senPlatformMessage方法里sendPlatformMessage()的第3个参数[2.4],是一个闭包函数如下所示。

(ByteData reply) {
  try {
    completer.complete(reply);
  } catch (exception, stack) {
    ...
  }
}

到这里就是触发Future的返回结果了,dart那边就会从await的地方继续执行。

7. 参考
深入理解Flutter的Platform Channel机制

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
FlutterMethodChannel是一种在Flutter和原生平台(如iOS)之间进行通信的机制。它允许Flutter应用程序调用原生平台上的方法,并且还可以让原生平台调用Flutter的方法。 在iOS上使用MethodChannel,首先需要在原生平台(Objective-C或Swift)的代码中创建一个MethodChannel实例。这个实例需要一个唯一的通道名称,以便Flutter可以识别它。然后,可以使用MethodChannel实例来注册方法,并指定一个方法名称和一个回调函数来处理该方法被调用时的逻辑。 在Flutter应用程序中,可以使用MethodChannel的实例来调用原生平台上注册的方法。可以指定方法名称和传递参数(如果需要)。MethodChannel会将这个方法调用发送到原生平台,并等待原生平台返回结果。一旦结果返回,可以在Flutter端处理它。 使用MethodChannel时需要注意一些事项。首先,MethodChannel只能传递符合平台限制的数据类型,如字符串、数字、布尔值等。如果需要传递复杂的数据结构,可以将数据转换为平台能够识别的格式(如JSON),然后再进行传递。其次,MethodChannel是一种异步通信机制,所以在处理方法调用时需要考虑异步操作和结果处理的情况。 总的来说,FlutterMethodChannel是一种强大而灵活的机制,可以实现Flutter应用程序与原生平台之间的双向通信。它为开发人员提供了在Flutter和iOS之间传递方法调用和数据的能力,可以实现更高级的功能和交互体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值