[-Flutter外掛篇 2-] 認識MethodChannel

張風捷特烈發表於2019-08-12

上次從一個路徑外掛看來一下Flutter中如何呼叫iOS和Android中的方法以及平臺如何返回值給Flutter框架。今天就來詳細講講MethodChannel是如何連同另一個世界的。


1.從吐司彈框開始說起(Android端/Java)

想要達成的效果是這樣使用可以彈出一個時間較長的吐司
這個示例要講述的是Flutter中如何向平臺傳遞引數

[-Flutter外掛篇 2-] 認識MethodChannel

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中就可以開心的彈吐司了

[-Flutter外掛篇 2-] 認識MethodChannel

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溝通的圖

[-Flutter外掛篇 2-] 認識MethodChannel

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進行模擬一個吐司框

[-Flutter外掛篇 2-] 認識MethodChannel

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,所以關係如下

[-Flutter外掛篇 2-] 認識MethodChannel

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,它是一個介面,定義了發生資訊的三個方法。
和資訊傳送相關的類有四個:

[-Flutter外掛篇 2-] 認識MethodChannel

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,期待與你的交流與切磋。

相關文章