上次从一个路径插件看来一下Flutter中如何调用iOS和Android中的方法以及平台如何返回值给Flutter框架。今天就来详细讲讲MethodChannel是如何连同另一个世界的。
1.从吐司弹框开始说起(Android端/Java)
代码语言:javascript复制想要达成的效果是这样使用可以弹出一个时间较长的吐司 这个示例要讲述的是Flutter中如何向平台传递参数
var show = RaisedButton(
onPressed: () {
IaToast.show(msg: "hello",type: Toast.LENGTH_LONG);
},
child: Text("点击弹吐司"),
);
1.1.Flutter/Dart端
代码语言:javascript复制定义一个
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端
代码语言:javascript复制通过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:使用效果
代码语言:javascript复制这样对应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:创建插件类:
代码语言:javascript复制现在来看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:自定义吐司
代码语言:javascript复制使用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的成员
代码语言:javascript复制其中有三个成员变量,我们在使用时只是传来一个字符串而已,其实还有两个是默认的 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方法
代码语言:javascript复制首先它是一个异步方法,传递方法名和参数,可以看出首先由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
代码语言:javascript复制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的编码方法
代码语言:javascript复制可以看出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发送信息
代码语言:javascript复制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
代码语言:javascript复制这里使用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
代码语言:javascript复制在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:设置方法监听处理器
代码语言:javascript复制监听器是设置在了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与回调参数的生成
代码语言:javascript复制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:信息发送追踪
代码语言:javascript复制到这里一切矛头指向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
代码语言:javascript复制我们在创建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
代码语言:javascript复制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
代码语言:javascript复制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的整体认知也有所提升。