上次從一個路徑外掛看來一下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
,期待與你的交流與切磋。