[-Flutter插件篇 -] 认识MethodChannel

11,159 阅读8分钟

上次从一个路径插件看来一下Flutter中如何调用iOS和Android中的方法以及平台如何返回值给Flutter框架。今天就来详细讲讲MethodChannel是如何连同另一个世界的。


1.从吐司弹框开始说起(Android端/Java)

想要达成的效果是这样使用可以弹出一个时间较长的吐司
这个示例要讲述的是Flutter中如何向平台传递参数

var show = RaisedButton(
    onPressed: () {
      IaToast.show(msg: "hello",type: Toast.LENGTH_LONG);
    },
    child: Text("点击弹吐司"),
);

1.1.Flutter/Dart端

定义一个IaToast的吐司类,根据枚举类型使用MethodChannel调用原生方法

import 'package:flutter/services.dart';

///吐司类型 [LENGTH_SHORT]短时间,[LENGTH_LONG]长时间
enum Toast { 
  LENGTH_SHORT,
  LENGTH_LONG
}

///吐司类
class IaToast {
  static const MethodChannel _channel =//方法渠道名
      const MethodChannel('www.toly1994.com.flutter_journey.toast');

  static show(//静态方法显示吐司
      {String msg, Toast type = Toast.LENGTH_SHORT}) {
    if (type == Toast.LENGTH_SHORT) {
      _channel.invokeMethod('showToast', {//渠道对象调用方法
        "msg": msg,
        "type": 0,
      });
    } else {
      _channel.invokeMethod('showToast', {
        "msg": msg,
        "type": 1,
      });
    }
  }
}

1.2:Android/Java端

通过FlutterView和渠道名可以获取MethodChannel对象,对其进行方法调用监听
其中的两个回调参数分别储存着方法信息和返回信息。

public class MainActivity extends FlutterActivity {
  private static final String CHANNEL = "www.toly1994.com.flutter_journey.toast";//渠道名
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
      MethodChannel channel = new MethodChannel(getFlutterView(), CHANNEL);//获取渠道
      channel.setMethodCallHandler(this::handleMethod);//设置方法监听
  }
    /**
     * 处理方法回调监听
     * @param methodCall 方法的参数相关
     * @param result 方法的返回值相关
     */
    private void handleMethod(MethodCall methodCall, MethodChannel.Result result) {
        switch (methodCall.method){//根据方法名进行处理
            case "showToast":
                handleToast(this,methodCall);//具体处理
                break;
            default:
                result.notImplemented();
        }
    }
    
    public static void handleToast(Context context,MethodCall methodCall) {
        String msg=methodCall.argument("msg");
        int type=methodCall.argument("type");
        Toast.makeText(context, msg, type).show();
    }
}

1.3:使用效果

这样对应Android端,在Flutter中就可以开心的弹吐司了

var show = RaisedButton(
  onPressed: () {
    IaToast.show(msg: "hello Flutter", type: Toast.LENGTH_LONG);//使用吐司
  },
  child: Text("点击弹吐司"),
);

var app = MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  home: Scaffold(
    appBar: AppBar(
      title: Text('Flutter之旅'),
    ),
    body: show,
  ),
);

void main() => runApp(app);

2.从吐司弹框开始说起(iOS端/Swift)

也简单的画了一幅Flutter和iOS沟通的图

2.1:创建插件类:

现在来看iOS端如何接受Flutter中的参数,和Android中基本一致,首先要获得渠道
在iOS里FlutterMethodChannel通过渠道标识和FlutterViewController来获取。
有了渠道方法之后,剩下的就几乎一致了,只是语法问题。
通过FlutterMethodCall回调中的call中的arguments值来获取参数,强转成NSDictionary
不过iOS系统并没有直接弹吐司的方法,所以需要自定义吐司。

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    public static let channelId="www.toly1994.com.flutter_journey.toast"
    
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
   
    let controller:FlutterViewController = window.rootViewController as! FlutterViewController
    let messageChannel = FlutterMethodChannel.init(//获取方法渠道
        name: AppDelegate.channelId,
        binaryMessenger:controller)
    
    messageChannel.setMethodCallHandler{(call, result) in
        self.handle(call,result)
    }
    
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
    
    public func handle(_ call: FlutterMethodCall,_ result: @escaping FlutterResult) {
        let args: NSDictionary = call.arguments as! NSDictionary
        switch call.method {
        case "showToast":
            let msg:String = args["msg"] as! String
            let type:Int = args["type"] as! Int
            handleToast(msg:msg,type:type)
        default:
            result(FlutterMethodNotImplemented)
        }
    }
    
    public func handleToast(msg: String, type: Int) {
        Toast.toast(text: msg,type:type)
    }
}

2.2:自定义吐司

使用UILabel和UIButton进行模拟一个吐司框

import UIKit
let toastDispalyDuration: CGFloat = 2.0
let toastBackgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
class Toast: NSObject {
    var duration: CGFloat = toastDispalyDuration
     var contentView: UIButton//内容框
    
    init(text: String) {
        let rect = text.boundingRect(
            with: CGSize(width: 250, height: CGFloat.greatestFiniteMagnitude),
            attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20)],//该值可调节边距
            context: nil)
    
        let textLabel = UILabel(//标签
            frame: CGRect(x: 0, y: 0, width: rect.size.width + 40, height: rect.size.height + 20))
        textLabel.backgroundColor = UIColor.clear
        textLabel.textColor = UIColor.white
        textLabel.textAlignment = .center
        textLabel.font = UIFont.systemFont(ofSize: 16)
        textLabel.text = text
        textLabel.numberOfLines = 0
        
       
        contentView = UIButton(type: .roundedRect)
        contentView.frame=CGRect(x: 0, y: 0,
            width: textLabel.frame.size.width,
            height: textLabel.frame.size.height)
        contentView.layer.cornerRadius = 15
        contentView.backgroundColor = toastBackgroundColor
        contentView.addSubview(textLabel)
        contentView.autoresizingMask = UIView.AutoresizingMask.flexibleWidth
        super.init()
        contentView.addTarget(self, action: #selector(toastTaped), for: .touchDown)
        NotificationCenter.default.addObserver(self, selector: #selector(toastTaped), name: UIDevice.orientationDidChangeNotification, object: UIDevice.current)
    }
    @objc func toastTaped() {
        self.hideAnimation()
    }
    func deviceOrientationDidChanged(notify: Notification) {
        self.hideAnimation()
    }
    @objc func dismissToast() {
        contentView.removeFromSuperview()
    }
    func setDuration(duration: CGFloat) {
        self.duration = duration
    }
    
    func showAnimation() {
        UIView.beginAnimations("show", context: nil)
        UIView.setAnimationCurve(UIView.AnimationCurve.easeIn)
        UIView.setAnimationDuration(0.3)
        contentView.alpha = 1.0
        UIView.commitAnimations()
    }
    @objc func hideAnimation() {
        UIView.beginAnimations("hide", context: nil)
        UIView.setAnimationCurve(UIView.AnimationCurve.easeOut)
        UIView.setAnimationDelegate(self)
        UIView.setAnimationDidStop(#selector(dismissToast))
        UIView.setAnimationDuration(0.3)
        contentView.alpha = 0.0
        UIView.commitAnimations()
    }

    func showFromBottomOffset(bottom: CGFloat) {
        let window: UIWindow = UIApplication.shared.windows.last!
        contentView.center = CGPoint(x: window.center.x, y: window.frame.size.height - (bottom + contentView.frame.size.height/2))
        window.addSubview(contentView)
        self.showAnimation()
        self.perform(#selector(hideAnimation), with: nil, afterDelay: TimeInterval(duration))
    }

    class func toast(text: String,type: Int) {
        let toast = Toast(text: text)
        var duration=0
        if type==0 {duration=1}else{duration=3}
        toast.setDuration(duration: CGFloat(duration))
        toast.showFromBottomOffset(bottom: 60)
    }
}

现在应该对MethodChannel有了一个感性的认知了,它可以连通Flutter框架和平台。


3.Flutter视角看MethodChannel

在Flutter中MethodChannel是一个Dart类,
处于flutter/lib/src/services/platform_channel.dart文件中

3.1:MethodChannel的成员

其中有三个成员变量,我们在使用时只是传来一个字符串而已,其实还有两个是默认的
codec是消息的编解码器,类型MethodCodec,默认是StandardMethodCodec
binaryMessenger是二进制信使,类型BinaryMessenger,默认是defaultBinaryMessenger

class MethodChannel {
  const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), this.binaryMessenger = defaultBinaryMessenger ])
    : assert(name != null),
      assert(binaryMessenger != null),
      assert(codec != null);

  final String name;
  final MethodCodec codec;
  final BinaryMessenger binaryMessenger;

3.2:MethodChannel的invokeMethod方法

首先它是一个异步方法,传递方法名和参数,可以看出首先由codec编码MethodCall对象
然后通过binaryMessenger去发送信息,获取的结构是一个字节数据,
如果结果非空,通过codec去解码,然后进行返回,可见这个泛型便是期望的结果类型

  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;
  }

3.3:MethodCodec类及StandardMethodCodec

MethodCodec是一个抽象接口,定义了编解码的方法,所以具体逻辑还要看它的实现类
MethodCodec有两个实现类StandardMethodCodec和JSONMethodCodec

abstract class MethodCodec {

  ByteData encodeMethodCall(MethodCall methodCall);
  MethodCall decodeMethodCall(ByteData methodCall);
  dynamic decodeEnvelope(ByteData envelope);
  ByteData encodeSuccessEnvelope(dynamic result);
  ByteData encodeErrorEnvelope({ @required String code, String message, dynamic details });
}
  • StandardMethodCodec的编码方法

可以看出StandardMethodCodec对MethodCall的编码是通过messageCodec实现的
messageCodec是StandardMessageCodec对象,其中的writeValue是编码的核心方法
将方法名和参数根据类型放入buffer中,从而将这些方法信息存储其中。


class StandardMethodCodec implements MethodCodec {
  
  const StandardMethodCodec([this.messageCodec = const StandardMessageCodec()]);

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

---->[StandardMessageCodec#writeValue]----
void writeValue(WriteBuffer buffer, dynamic value) {
  if (value == null) {
    buffer.putUint8(_valueNull);
  } else if (value is bool) {
    buffer.putUint8(value ? _valueTrue : _valueFalse);
  } else if (value is int) {
    if (-0x7fffffff - 1 <= value && value <= 0x7fffffff) {
      buffer.putUint8(_valueInt32);
      buffer.putInt32(value);
    } else {
      buffer.putUint8(_valueInt64);
      buffer.putInt64(value);
    }
  //略...
  } else if (value is List) {
    buffer.putUint8(_valueList);
    writeSize(buffer, value.length);
    for (final dynamic item in value) {
      writeValue(buffer, item);
    }
  } else if (value is Map) {
    buffer.putUint8(_valueMap);
    writeSize(buffer, value.length);
    value.forEach((dynamic key, dynamic value) {
      writeValue(buffer, key);
      writeValue(buffer, value);
    });
  } else {
    throw ArgumentError.value(value);
  }
}

3.5:BinaryMessages发送信息

BinaryMessenger是一个抽象接口,默认使用的实现了是defaultBinaryMessenger
_sendPlatformMessage方法进行对平台发送信息

const BinaryMessenger defaultBinaryMessenger = _DefaultBinaryMessenger._();

---->[BinaryMessenger]----
abstract class BinaryMessenger {
  const BinaryMessenger();
  Future<void> handlePlatformMessage(String channel, ByteData data, ui.PlatformMessageResponseCallback callback);
  Future<ByteData> send(String channel, ByteData message);
  void setMessageHandler(String channel, Future<ByteData> handler(ByteData message));
  void setMockMessageHandler(String channel, Future<ByteData> handler(ByteData message));
}

---->[_DefaultBinaryMessenger]----
@override
Future<ByteData> send(String channel, ByteData message) {
  final MessageHandler handler = _mockHandlers[channel];
  if (handler != null)
    return handler(message);
  return _sendPlatformMessage(channel, message);
}
  • _sendPlatformMessage

这里使用Window对象进行信息发送,最终调用的是Window_sendPlatformMessage的native方法

final Window window = Window._();

Future<ByteData> _sendPlatformMessage(String channel, ByteData message) {
  final Completer<ByteData> completer = Completer<ByteData>();
  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;
}

---->[Window#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';

4.Android视角看MethodChannel

在Android中MethodChannel是一个Java类,处于io.flutter.plugin.common
主要的成员变量也是三位messenger,name和codec,在构造方法中需要传入BinaryMessenger
默认的MethodCodec是StandardMethodCodec.INSTANCE

public final class MethodChannel {
    private static final String TAG = "MethodChannel#";
    private final BinaryMessenger messenger;
    private final String name;
    private final MethodCodec codec;

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

4.1:设置方法监听处理器

监听器是设置在了messenger的身上,如果监听器非空会使用IncomingMethodCallHandler
messenger需要的监听器的类型是BinaryMessenger.BinaryMessageHandler,所以关系如下

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

---->[BinaryMessenger]----
public interface BinaryMessenger {
    @UiThread
    void setMessageHandler(@NonNull String var1,
    @Nullable BinaryMessenger.BinaryMessageHandler var2);

4.2:IncomingMethodCallHandler与回调参数的生成

IncomingMethodCallHandler实现了BinaryMessageHandler接口,必然实现其接口方法
onMessage中需要回调了ByteBuffer的方法字节信息以及BinaryReply对象
回调中的MethodCall对象是通过codec将字节信息解码生成的
MethodChannel.Result是一个接口,有三个接口方法,这里直接new对象并实现三个方法
通过codec编码success传入的对象,后通过reply对象的reply将返回值传给Flutter端

private final class IncomingMethodCallHandler implements BinaryMessageHandler {
    private final MethodChannel.MethodCallHandler handler;
    IncomingMethodCallHandler(MethodChannel.MethodCallHandler handler) {
        this.handler = handler;
    }
    @UiThread
    public void onMessage(ByteBuffer message, final BinaryReply reply) {
        MethodCall call = MethodChannel.this.codec.decodeMethodCall(message);
        try {
            this.handler.onMethodCall(call, new MethodChannel.Result() {
                public void success(Object result) {
                    reply.reply(MethodChannel.this.codec.encodeSuccessEnvelope(result));
                }
                public void error(String errorCode, String errorMessage, Object errorDetails) {
                    reply.reply(MethodChannel.this.codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails));
                }
                public void notImplemented() {
                    reply.reply((ByteBuffer)null);
                }
            });
        } catch (RuntimeException var5) {
            Log.e("MethodChannel#" + MethodChannel.this.name, "Failed to handle method call", var5);
            reply.reply(MethodChannel.this.codec.encodeErrorEnvelope("error", var5.getMessage(), (Object)null));
        }
    }

5:信息发送追踪

到这里一切矛头指向BinaryMessenger,它是一个接口,定义了发生信息的三个方法。
和信息发送相关的类有四个:

public interface BinaryMessenger {
    @UiThread
    void send(@NonNull String var1, @Nullable ByteBuffer var2);

    @UiThread
    void send(@NonNull String var1, @Nullable ByteBuffer var2, @Nullable BinaryMessenger.BinaryReply var3);

    @UiThread
    void setMessageHandler(@NonNull String var1, @Nullable BinaryMessenger.BinaryMessageHandler var2);

    public interface BinaryReply {
        @UiThread
        void reply(@Nullable ByteBuffer var1);
    }

    public interface BinaryMessageHandler {
        @UiThread
        void onMessage(@Nullable ByteBuffer var1, @NonNull BinaryMessenger.BinaryReply var2);
    }
}

5.1:FlutterView

我们在创建MethodChannel的时候传入的是getFlutterView()
追踪一下可以看到返回的是一个FlutterView,这也就说明FlutterView实现了BinaryMessenger
所以可以从实现的方法入手,最终发现是调用mNativeView的方法,其为FlutterNativeView类型

MethodChannel channel = new MethodChannel(getFlutterView(), CHANNEL);//获取渠道

---->[FlutterActivity]----
public FlutterView getFlutterView() {
    return this.viewProvider.getFlutterView();
}

---->[FlutterView]----
public interface Provider {
    FlutterView getFlutterView();
}

---->[FlutterView]----
@UiThread
public void send(String channel, ByteBuffer message) {
    this.send(channel, message, (BinaryReply)null);
}

@UiThread
public void send(String channel, ByteBuffer message, BinaryReply callback) {
    if (!this.isAttached()) {
        Log.d("FlutterView", "FlutterView.send called on a detached view, channel=" + channel);
    } else {
        this.mNativeView.send(channel, message, callback);
    }
}

5.2:FlutterNativeView与DartExecutor

FlutterNativeView调用dartExecutor的方法,其为DartExecutor类型
在构造方法中创建了FlutterJNI对象来创建DartExecutor,
DartExecutor中通过DartMessenger对象messenger发送,这些DartMessenger跑不掉了

public class FlutterNativeView implements BinaryMessenger {
 
    private final DartExecutor dartExecutor;
    private final FlutterJNI mFlutterJNI;
    
    public FlutterNativeView(@NonNull Context context) {
        this(context, false);
    }
    public FlutterNativeView(@NonNull Context context, boolean isBackgroundView) {
        this.mContext = context;
        this.mPluginRegistry = new FlutterPluginRegistry(this, context);
        this.mFlutterJNI = new FlutterJNI();
        this.mFlutterJNI.setRenderSurface(new FlutterNativeView.RenderSurfaceImpl());
        this.dartExecutor = new DartExecutor(this.mFlutterJNI);

---->[FlutterNativeView]----
@UiThread
public void send(String channel, ByteBuffer message) {
    this.dartExecutor.send(channel, message);
}
@UiThread
public void send(String channel, ByteBuffer message, BinaryReply callback) {
    if (!this.isAttached()) {
        Log.d("FlutterNativeView", "FlutterView.send called on a detached view, channel=" + channel);
    } else {
        this.dartExecutor.send(channel, message, callback);
    }
}

---->[DartExecutor]----
@UiThread
public void send(@NonNull String channel, @Nullable ByteBuffer message) {
    this.messenger.send(channel, message, (BinaryReply)null);
}
@UiThread
public void send(@NonNull String channel, @Nullable ByteBuffer message, @Nullable BinaryReply callback) {
    this.messenger.send(channel, message, callback);
}

5.3:DartMessenger

DartMessenger通过flutterJNI.dispatchPlatformMessage发送信息
最终到nativeDispatchPlatformMessage一个native方法,
然后那些C++里见不得人的勾当这里就不说了,有机会再细细道来。

@UiThread
public void send(@NonNull String channel, @NonNull ByteBuffer message) {
    Log.v("DartMessenger", "Sending message over channel '" + channel + "'");
    this.send(channel, message, (BinaryReply)null);
}
public void send(@NonNull String channel, @Nullable ByteBuffer message, @Nullable BinaryReply callback) {
    Log.v("DartMessenger", "Sending message with callback over channel '" + channel + "'");
    int replyId = 0;
    if (callback != null) {
        replyId = this.nextReplyId++;
        this.pendingReplies.put(replyId, callback);
    }
    if (message == null) {
        this.flutterJNI.dispatchEmptyPlatformMessage(channel, replyId);
    } else {
        this.flutterJNI.dispatchPlatformMessage(channel, message, message.position(), replyId);
    }
}

@UiThread
public void dispatchPlatformMessage(@NonNull String channel, @Nullable ByteBuffer message, int position, int responseId) {
    this.ensureRunningOnMainThread();
    if (this.isAttached()) {
        this.nativeDispatchPlatformMessage(this.nativePlatformViewId, channel, message, position, responseId);
    } else {
        Log.w("FlutterJNI", "Tried to send a platform message to Flutter, but FlutterJNI was detached from native C++. Could not send. Channel: " + channel + ". Response ID: " + responseId);
    }
}

源码贴的有点多,整个关系看起来也不是非常复杂。虽然没啥大用,逻辑捋一捋对Flutter的整体认知也有所提升。



结语

本文到此接近尾声了,如果想快速尝鲜Flutter,《Flutter七日》会是你的必备佳品;如果想细细探究它,那就跟随我的脚步,完成一次Flutter之旅。
另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,本人微信号:zdl1994328,期待与你的交流与切磋。