flutter platform channel MethodChannel 源碼解析

Flutter是Google使用Dart語言開發的一套移動應用開發框架。它不同于其他開發框架:

  • 因為Flutter使用AOT預編譯代碼為機器碼,所以它的運行效率更高。

  • Flutter的UI控件并沒有使用底層的原生控件,而是使用Skia渲染引擎繪制而成,因為不依賴底層控件,所以多端一致性非常好。

  • Flutter的擴展性也非常強,開發者可以通過Plugin與Native進行通信。

參考深入理解Flutter Platform Channel

Platform Channel 主要用于Flutter和原生端的數據傳遞。

Flutter定義了三種不同類型的Channel,分別是

  • BasicMessageChannel:用于傳遞字符串和半結構化的數據;
  • MethodChannel:用于傳遞方法調用;
  • EventChannel:用于數據流的通信;

消息使用平臺通道在客戶端(UI)和宿主(平臺)之間傳遞,如下圖所示:


根據架構圖,我們可以看出在Flutter端,MethodChannel 允許發送與方法調用相對應的消息。Android上的MethodChannel 啟用接收方法調用并發回結果給Flutter端。而這種數據傳遞方式還可以反向調用。為了保證用戶界面保持相應而不卡頓,消息和響應以異步的形式進行傳遞。
以確保用戶界面能夠保持響應。

Flutter 是通過 Dart 異步發送消息的。即便如此,當你調用一個平臺方法時,也需要在主線程上做調用。

具體使用方法請參考Demo實現了一個從Flutter端發起的方法調用,從原生端獲取數據并返回給Flutter端用于展示。以下是基本使用方法。

//flutter 代碼
class MethodChannelManager {
  MethodChannel _methodChannel;

  static MethodChannelManager _instance;
  String _methodChannelName = "flutter_method_channel";

  MethodChannelManager._internal() {
    if (_methodChannel == null) {
      _methodChannel = new MethodChannel(_methodChannelName);
    }
  }

  static MethodChannelManager getInstance() {
    if (_instance == null) {
      _instance = new MethodChannelManager._internal();
    }
    return _instance;
  }

  Future<String> sendMessage() async {
    Map<String, String> map = {"flutter": "這是一條來自flutter的參數"};
    String message = await _methodChannel.invokeMethod("success", map);
    return message;
  }

  Future<double> getRefreshRate() async {
    return MethodChannel('fps_plugin').invokeMethod("getRefreshRate");
  }
}


//android 原生端代碼

public class NativeBasePlugin implements MethodCallHandler, StreamHandler, FlutterPlugin , ActivityAware {

    private Context applicationContext;

    private MethodChannel methodChannel;
    private EventChannel eventChannel;
    public static String methodChannelName = "flutter_method_channel";
    public static String eventChannelName = "flutter_event_channel";


    public int index = 0;

    /**
     * Plugin registration.
     */
    public static void registerWith(FlutterEngine flutterEngine) {
        final NativeBasePlugin instance = new NativeBasePlugin();
        instance.onAttachedToEngine(new ShimPluginRegistry(flutterEngine).registrarFor(methodChannelName).context(),
                flutterEngine.getDartExecutor());
    }

    @Override
    public void onAttachedToEngine(FlutterPluginBinding binding) {
        onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger());

    }

    private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) {
        this.applicationContext = applicationContext;

        methodChannel = new MethodChannel(messenger, methodChannelName);
        methodChannel.setMethodCallHandler(this);

        eventChannel = new EventChannel(messenger, eventChannelName);
        eventChannel.setStreamHandler(this);

    }

    @Override
    public void onMethodCall(MethodCall methodCall, Result result) {
        switch (methodCall.method) {
            case "success":
                //解析參數
                String value = methodCall.argument("flutter");
                // Toast.makeText(applicationContext, "" + value, Toast.LENGTH_SHORT).show();
                result.success("A");
                break;
            default:
                result.notImplemented();
        }
    }

    @Override
    public void onDetachedFromEngine(FlutterPluginBinding binding) {
        applicationContext = null;
        methodChannel.setMethodCallHandler(null);
        methodChannel = null;
        eventChannel.setStreamHandler(null);
        eventChannel = null;
    }

    @Override
    public void onListen(Object arguments, EventSink events) {

        /**
         * CountDownTimer timer = new CountDownTimer(3000, 1000)中,
         * 第一個參數表示總時間,第二個參數表示間隔時間。
         * 意思就是每隔一秒會回調一次方法onTick,然后1秒之后會回調onFinish方法。
         */
        CountDownTimer timer = new CountDownTimer(20000, 1000) {
            public void onTick(long millisUntilFinished) {
                events.success("index=" + millisUntilFinished / 1000 + "秒");

            }

            public void onFinish() {
                events.success("時間結束了");
            }
        };
        timer.start();
    }

    @Override
    public void onCancel(Object arguments) {
        //EventChannel 取消
        index = 0;

    }
}

Dart層方法調用的消息傳遞分析

首先,dart中會先創建一個MethodChannel對象,其名稱為flutter_method_channel”,這個名字很關鍵,必須與原生端的名字相對應,具體原因后邊會有解釋。通過異步方式調用invokeMethod方法傳入方法名來獲取信息 await _methodChannel.invokeMethod('success');

invokeMethod方法具體實現如下

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

通過BinaryMessages.send()方法來發送方法調用消息,我們可以看到send方法有兩個參數,第一個是channel的名稱,第二個是ByteData對象(使用codec對根據方法名和參數構建的MethodCall對象進行編碼得到的對象);codec對象是在MethodChannel對象創建時默認創建的StandardMethodCodec對象,其對MethodCall對象的編碼過程如下



  const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger binaryMessenger ])
      : assert(name != null),
        assert(codec != null),
        _binaryMessenger = binaryMessenger;

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

通過messageCodec將調用的方法名和傳遞的參數寫入到buffer中,messageCodec是一個StandardMessageCodec對象,在StandardMethodCodec對象創建時默認創建,其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 double) {  // Double precedes int because in JS everything is a double.
                                   // Therefore in JS, both `is int` and `is double` always
                                   // return `true`. If we check int first, we'll end up treating
                                   // all numbers as ints and attempt the int32/int64 conversion,
                                   // which is wrong. This precedence rule is irrelevant when
                                   // decoding because we use tags to detect the type of value.
      buffer.putUint8(_valueFloat64);
      buffer.putFloat64(value);
    } 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 String) {
      buffer.putUint8(_valueString);
      final List<int> bytes = utf8.encoder.convert(value);
      writeSize(buffer, bytes.length);
      buffer.putUint8List(bytes);
    } else if (value is Uint8List) {
      buffer.putUint8(_valueUint8List);
      writeSize(buffer, value.length);
      buffer.putUint8List(value);
    } else if (value is Int32List) {
      buffer.putUint8(_valueInt32List);
      writeSize(buffer, value.length);
      buffer.putInt32List(value);
    } else if (value is Int64List) {
      buffer.putUint8(_valueInt64List);
      writeSize(buffer, value.length);
      buffer.putInt64List(value);
    } else if (value is Float64List) {
      buffer.putUint8(_valueFloat64List);
      writeSize(buffer, value.length);
      buffer.putFloat64List(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);
    }
  }

上述代碼看出,Flutter與平臺端的消息傳遞支持12種類型,這12種類型分別與安卓和iOS中的類型相對應,看下面表格更直觀


writeValue方法其實就是將方法名和參數轉化為對應的二進制數據寫入buffer中,方法名都是String類型,我們就以String類型方法寫入過程來進行簡單說明,如果判斷一個value為String后,

1、調用buffer.putUint8(_valueString);先寫入對應的類型值,_valueString = 7;,所以將00000111二進制數據寫入buffer;

2、緊接著將value通過utf8編碼為int數組,然后將數組的長度數據通過writeSize(buffer, bytes.length);寫入buffer;

3、最后再將數組數據寫入buffer,至此一個方法名編碼完成;

其他類型的數據依次類推進行編碼,編碼完成后,將StandardMessageCodec對象編碼的ByteData數據通過BinaryMessages.send()方法發送出去,看下send方法的具體實現

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

會從_mockHandlers中查找是否緩存的有_MessageHandler對象,如果沒有則通過_sendPlatformMessage方法發送消息,

消息處理器:Handler

當我們接收二進制格式消息并使用Codec將其解碼為Handler能處理的消息后,就該Handler上場了。Flutter定義了三種類型的Handler,與Channel類型一一對應。我們向Channel注冊一個Handler時,實際上就是向BinaryMessager注冊一個與之對應的BinaryMessageHandler。當消息派分到BinaryMessageHandler后,Channel會通過Codec將消息解碼,并傳遞給Handler處理。

  • MessageHandler

MessageHandler用戶處理字符串或者半結構化的消息,其onMessage方法接收一個T類型的消息,并異步返回一個相同類型result。MessageHandler的功能比較基礎,使用場景較少,但是其配合BinaryCodec使用時,能夠方便傳遞二進制數據消息。

  • MethodHandler

MethodHandler用于處理方法的調用,其onMessage方法接收一個MethodCall類型消息,并根據MethodCall的成員變量method去調用對應的API,當處理完成后,根據方法調用成功或失敗,返回對應的結果。

  • StreamHandler

StreamHandler與前兩者稍顯不同,用于事件流的通信,最為常見的用途就是Platform端向Flutter端發送事件消息。當我們實現一個StreamHandler時,需要實現其onListen和onCancel方法。而在onListen方法的入參中,有一個EventSink(其在Android是一個對象)。我們持有EventSink后,即可通過EventSink向Flutter端發送事件消息。

下面再來看下_sendPlatformMessage方法,其最終調用的是ui.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';

在以上代碼中ui.window.sendPlatformMessage()方法最終會調用Dart本地接口方法_sendPlatformMessage,這里可以將這個方法簡單理解為類似于java的JNI的方法,在c++層會調用"Window_sendPlatformMessage"對應的方法。至此,dart中的方法消息傳遞已經結束,

c++層是如何對方法調用消息進行傳遞的。這里不做具體分析,有興趣可以從Flutter engine源碼中分析

接下來我們開始分析java層接受到消息后的處理邏輯。

c++層通過調用flutterJNI的handlePlatformMessage方法將channel名稱、消息內容和響應ID傳給java層,我們來看一下FlutterJNI中的方法實現

    private void handlePlatformMessage(@NonNull String channel, byte[] message, int replyId) {
        if (this.platformMessageHandler != null) {
            this.platformMessageHandler.handleMessageFromDart(channel, message, replyId);
        }

    }

此時會調用platformMessageHandler的handleMessageFromDart()方法,handleMessageFromDart 是PlatformMessageHandler 接口里面的一個方法 在DartMessenger 里面實現

   public void handleMessageFromDart(@NonNull String channel, @Nullable byte[] message, int replyId) {
        Log.v("DartMessenger", "Received message from Dart over channel '" + channel + "'");
        //首先根據channel名稱從mMessageHandlers中查找對應的BinaryMessageHandler對象
        BinaryMessageHandler handler = (BinaryMessageHandler)this.messageHandlers.get(channel);
        if (handler != null) {
            try {
                Log.v("DartMessenger", "Deferring to registered handler to process message.");
                ByteBuffer buffer = message == null ? null : ByteBuffer.wrap(message);
              //如果找到則執行該對象的onMessage()方法,
                handler.onMessage(buffer, new DartMessenger.Reply(this.flutterJNI, replyId));
            } catch (Exception var6) {
                Log.e("DartMessenger", "Uncaught exception in binary message listener", var6);
                this.flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
            }
        } else {
            Log.v("DartMessenger", "No registered handler for message. Responding to Dart with empty reply message.");
            this.flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
        }

    }


我們主要看 mMessageHandlers中是怎么保存我們的channel名稱為flutter_method_channel 的對象的呢,我們看下開始所說的demo中的java模塊相關代碼,

    public static String methodChannelName = "flutter_method_channel";
    methodChannel.setMethodCallHandler(this);


   @Override
    public void onMethodCall(MethodCall methodCall, Result result) {
        switch (methodCall.method) {
            case "success":
                //解析參數
                String value = methodCall.argument("flutter");
                // Toast.makeText(applicationContext, "" + value, Toast.LENGTH_SHORT).show();
                result.success("A");
                break;
            default:
                result.notImplemented();
        }
    }


創建一個名為flutter_method_channel 的MethodChannel對象,然后設置對應的MethodCallHandler對象,setMethodCallHandler的方法實現如下

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

其中的messenger就是實現了BinaryMessenger接口的對象 再來看 this.messenger.setMessageHandler ,setMessageHandler 是一個接口,主要看他在DartMessenger 的實現類

    public void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler) {
        if (handler == null) {
            Log.v("DartMessenger", "Removing handler for channel '" + channel + "'");
            this.messageHandlers.remove(channel);
        } else {
            Log.v("DartMessenger", "Setting handler for channel '" + channel + "'");
            this.messageHandlers.put(channel, handler);
        }

    }

到此,我們發現在注冊插件方法中實現的 MethodCallHandler通過一系列操作被包裝到MethodChannel.IncomingMethodCallHandler對象中并設置進了mMessageHandlers中。那么我們上面所說的onMessage方法的調用即是MethodChannel.IncomingMethodCallHandler對象的方法,

    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) {
          //首先將從c++層傳遞過來的消息通過codec解碼為MethodCall對象
            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));
            }

        }
    }

方法中首先將從c++層傳遞過來的消息通過codec解碼為MethodCall對象,然后調用注冊的地方實現的MethodHandler的onMethodCall方法,該方法會接受flutter 傳遞的參數,

 case "success":
                //解析參數
                String value = methodCall.argument("flutter");
                // Toast.makeText(applicationContext, "" + value, Toast.LENGTH_SHORT).show();
                result.success("A");
                break;

然后調用result.success()方法,通過reply.reply(MethodChannel.this.codec.encodeSuccessEnvelope(result));將結果數據編碼后進行返回。reply方法中會調用
DartMessenger .this.mFlutterJNI.invokePlatformMessageResponseCallback(replyId, reply, reply.position());方法將響應結果返回,方法具體實現如下

 public void reply(@Nullable ByteBuffer reply) {
            if (this.done.getAndSet(true)) {
                throw new IllegalStateException("Reply already submitted");
            } else {
                if (reply == null) {
                    this.flutterJNI.invokePlatformMessageEmptyResponseCallback(this.replyId);
                } else {
                    this.flutterJNI.invokePlatformMessageResponseCallback(this.replyId, reply, reply.position());
                }

            }
        }

最終會調用JNI方法將數據返回給c++層,

Dart層接收消息響應后的處理流程分析

通過以上Dart層傳遞消息分析可知PlatformMessageResponseCallback方法回調后對byte_buffer數據進行處理,通過completer.complete()方法完成返回數據,然后一步步返回到調用方法層,在異步方法中通過await等待數據返回后,再通過setState改變State中的變量值從而刷新頁面數據信息顯示到屏幕上。至此,整個flutter發消息給platform并接收消息處理的流程就完成了。

簡約調用關系圖如下

channel (1).jpg

image.png

總結,整個消息發送和接收結果的流程分為以下幾步:

  • Dart層通過以上提到的12種類型包含的類型數據進行編碼,然后通過dart的類似jni的本地接口方法傳遞給c++層;
  • c++層通過持有java對象flutterJNI的方法調用將消息傳遞到java層;
  • java層解碼接收到的消息,根據消息內容做指定的邏輯處理,得到結果后進行編碼通過jni方法將響應結果返回給c++層;
  • c++層處理返回的響應結果,并將結果通過發送時保存的dart響應方法對象回調給Dart層;
    Dart層通過回調方法對結果數據進行處理,然后通過codec解碼數據做后續的操作;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370