Flutter Chanel通訊流程

楊充發表於2021-08-26

目錄介紹

  • 01.flutter和原生之間互動
  • 02.MethodChanel流程
  • 03.MethodChanel使用流程
  • 04.MethodChanel程式碼實踐
  • 05.EventChannel流程
  • 06.EventChannel基本流程
  • 07.EventChannel程式碼實現
  • 08.BasicMessageChannel流程
  • 09.BasicMessageChannel基本流程
  • 10.BasicMessageChannel程式碼實現
  • 11.Channel編解碼器說明
  • 12.Channel通訊可以子執行緒嗎
  • 13.Channel通訊傳遞穩定性
  • 14.onActivityResult如何實現

推薦

01.flutter和原生之間互動

1.1 互動簡單介紹

  • 官方給的通訊方式
    • 看圖片,channel通訊方式
    • 從底層來看,Flutter和平臺端通訊的方式是傳送非同步的二進位制訊息,該基礎通訊方式在Flutter端由BinaryMessages來實現, 而在Android端是一個介面BinaryMessenger,其具體實現為FlutterNativeView,在iOS端是一個協議 FlutterBinaryMessenger,FlutterViewController遵守並實現了這個協議。
  • flutter可以與native之間進行通訊,幫助我們使用native提供的能力。
    • 通訊是雙向的,我們可以從Native層呼叫flutter層的dart程式碼,同時也可以從flutter層呼叫Native的程式碼。
  • 我們需要使用Platform Channels APIs進行通訊,主要包括下面三種:
    • MethodChannel:用於傳遞方法呼叫(method invocation)
    • EventChannel:用於事件流的傳送(event streams)
    • BasicMessageChannel:用於傳遞字串和半結構化的訊息,這裡什麼叫做半結構化?下面會解釋……
  • channel通訊是非同步還是同步的
    • 為了保證使用者介面在互動過程中的流暢性,無論是從Flutter向Native端傳送訊息,還是Native向Flutter傳送訊息都是以非同步的形式進行傳遞的。那為何不使用同步來操作,下面會說到……
  • 幾種channel應用場景分析
    • MethodChannel使用場景:無論是Flutter端還是Native端都可以通過MethodChannel向對方平臺傳送兩端提前定義好的方法名來呼叫對方平臺相對應的訊息處理邏輯並且帶回返回值給被呼叫方。
    • EventChannel的使用場景:更側重於Native平臺主動向Flutter平臺,單向給Flutter平臺傳送訊息,Flutter無法返回任何資料給Native端,EventChannel描述是單通的。可以類比Android裡面的廣播……
    • BasicMessageChannel的使用場景:比如flutter想拍照,拍完照後的圖片路徑需要傳給flutter,照片的路徑傳送可以使用BasicMessageChannel.Reply回覆,也可以使用sendMessage主動再發一次訊息。個人認為接收訊息並回復訊息屬於一次通訊,所以傾向於使用BasicMessageChannel.Reply。
  • 混合開發通常用那種channel
    • 只是混合開發通常涉及到兩端頻繁通訊,個人更加傾向使用BasicMessageChannel,不分主客,使用和通訊更方便。

1.2 核心類重點說明

  • MethodCall
    • 方法呼叫Java層封裝,主要是資料類
  • MethodChannel
    • 這個主要使用者和dart進行方法通訊,類
  • MethodCallHandler
    • 這個java層處理dart層時間的介面,在通訊協議中屬於上層介面,介面
  • BinaryMessageHandler
    • java層和dart層通訊的最底層抽象介面,面向二進位制資料包,介面
  • DartMessenger
    • 最底層用於接收JNI傳送過來的資料。實現類
  • DartExecutor
    • 配置、引導並開始執行Dart程式碼。BinaryMessenger的具體實現類
  • FlutterView
    • NA用來承載flutter的容器view
  • IncomingMethodCallHandler
    • BinaryMessageHandler的實現類,使用者接收底層傳送過來的資料包,然後轉發給MethodCallHandler,並對MethodCallHandler 傳送過的結果進行打包傳送給dart層。實現類
  • FlutterJNI
    • JNI層的封裝用於跟底層引擎側進行通訊

02.MethodChannel流程

  • 其中最常用的是MethodChanel,MethodChanel的使用與在Android的JNI呼叫非常類似,但是MethodChanel更加簡單,而且相對於JNI的同步呼叫MethodChanel的呼叫是非同步的:
    • image
  • 從flutter架構圖上可以看到,flutter與native的通訊發生在Framework和Engine之間,framewrok內部會將MethodChannel以BinaryMessage的形式與Engine進行資料交換。

03.MethodChanel使用流程

3.1 flutter呼叫native

  • flutter呼叫native步驟
    • [native] 使用MethodChannel#setMethodCallHandler註冊回撥
    • [flutter] 通過MethodChannel#invokeMethod發起非同步呼叫
    • [native] 呼叫native方法通過Result#success返回Result,出錯時返回error
    • [flutter] 收到native返回的Result
  • 如圖所示
    • image

3.2 native呼叫flutter

  • native呼叫flutter
    • 與flutter呼叫native的順序完全一致,只是[native]與[flutter]角色反調
  • 如圖所示
    • image
  • NA端使用MethodChannel
    • 首先定義Channel名稱,需要保證是唯一的,在Flutter端需要使用同樣的名稱來建立MethodChannel。如果名稱不一樣,則會導致匹配不上……
    • 第一個引數:是messenger,型別是BinaryMessenger,是一個介面,代表訊息信使,是訊息傳送與接收的工具;
    • 第二個引數:是name,就是Channel名稱,和flutter定義的要一樣;
    • 第三個引數:是codec,型別是MethodCodec,代表訊息的編解碼器,如果沒有傳該引數,預設使用StandardMethodCodec。

04.MethodChanel程式碼實踐

4.1 native呼叫flutter

  • 定義好了MethodChannel之後呼叫setMethodCallHandler()方法設定訊息處理回撥,引數是MethodHandler型別,需要實現它的onMethodCall()方法。onMethodCall()方法有兩個引數methodCall和result,methodCall記錄了呼叫的方法資訊,包括方法名和引數,result用於方法的返回值,可以通過result.success()方法返回資訊給Flutter端。
    private void createChannel() {
        nativeChannel = new MethodChannel(binaryMessenger, METHOD_CHANNEL, StandardMethodCodec.INSTANCE);
        // 註冊Handler實現
        nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(@NonNull MethodCall methodCall, @NonNull MethodChannel.Result result) {
                if ("android".equals(methodCall.method)) {
                    //接收來自flutter的指令
                    String flutter = methodCall.argument("flutter");
                    //返回給flutter的引數
                    result.success("Na收到指令");
                } 
            }
        });
    }
    複製程式碼
  • 可以通過invokeMethod方法讓NA執行呼叫flutter方法。那麼執行了flutter方法後需要回傳資料,這個時候就需要用到Result介面呢,程式碼如下所示:
    HashMap<String , String> map = new HashMap<>();
    map.put("invokeKey","你好,這個是從NA傳遞過來的資料");
    //nativeChannel.resizeChannelBuffer(100);
    nativeChannel.invokeMethod("getFlutterResult", map , new MethodChannel.Result() {
        @SuppressLint("SetTextI18n")
        @Override
        public void success(@Nullable Object result) {
            tvContent.setText("測試內容:"+result);
        }
    
        @SuppressLint("SetTextI18n")
        @Override
        public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
            tvContent.setText("測試內容:flutter傳遞給na資料傳遞錯誤");
        }
    
        @Override
        public void notImplemented() {
    
        }
    });
    複製程式碼
  • 事件接收處理端
    • 接收處理回撥時onMethodCall(MethodCall call, MethodChannel.Result result)通過methodCall接收事件傳送者傳遞回來的資訊,通過Result把處理完的結果傳送給事件傳送方。
    • 通過methodCall.method:來區分不同函式名(方法)名以執行不同的業務邏輯,
    • 通過methodCall.hasArgument("key"):判斷是否有某個key對應的value
    • 通過methodCall.argument("key"):獲取key對應的value值
    • 通過result.success(object):把處理完的結果返回給事件傳送方
  • 事件傳送端
    • 處理事件傳送方通過methodChannel.invokeMethod("方法名","要傳遞的引數")把需要傳遞的引數傳遞給事件監聽者。 其中
    • 方法名:不能為空
    • 要傳遞的引數:可以為空,若不為空則必須為可Json序列化的物件。
    • callback:可以為空,若不為空則表示執行了flutter方法後的回撥監聽狀態

4.2 flutter呼叫native

  • Flutter使用MethodChannel
    • 在Flutter端同樣需要定義一個MethodChannel,使用MethodChannel需要引入services.dart包,Channel名稱要和Android端定義的相同。
    static const method = const MethodChannel('com.ycbjie.android/method');
    複製程式碼
  • 新增監聽NA呼叫flutter方法的監聽,flutter程式碼是setMethodCallHandler方法實現。return則表示flutter回傳給NA的資料操作。
      method.setMethodCallHandler(nativeCallHandler);
    
      // 註冊方法,等待被原生通過invokeMethod喚起
      Future<dynamic> nativeCallHandler(MethodCall methodCall) async {
        switch (methodCall.method) {
          case "getFlutterResult":
          //獲取引數
            String paramsFromNative = await methodCall.arguments["invokeKey"];
            print("原生android傳遞過來的引數為------ $paramsFromNative");
            return "你好,這個是從flutter回傳給NA的資料";
            break;
        }
      }
    複製程式碼
  • flutter是如何給NA傳送訊息的呢,直接呼叫invokeMethod方法,程式碼如下所示
      Future<Null> _jumpToNativeWithParams1() async {
        Map<String, String> map = { "flutter": "這是一條來自flutter的引數" };
        String result = await method.invokeMethod('android', map);
        print(result);
      }
    複製程式碼

05.EventChannel流程

  • EventChannel用於從native向flutter傳送通知事件,例如flutter通過其監聽Android的重力感應變化等。與MethodChannel不同,EventChannel是native到flutter的單向呼叫,呼叫是多播(一對多)的,可以類比成Android的Brodecast廣播。

06.EventChannel基本流程

  • 照例先看一下API使用的基本流程:
    • [native]EventChannel#setStreamHandler註冊Handler實現
    • [native]EventChannel初始化結束後,在StreamHandler#onLister回撥中獲取EventSink引用並儲存
    • [flutter]EventChannel#receiveBroadcastStream註冊listener,建立監聽
    • [native]使用EventSink#sucess傳送通知事件
    • [flutter]接受到事件通知
    • [native]通知結束時呼叫endOfStream結束
  • 如圖所示
    • image

07.EventChannel程式碼實現

  • flutter端
    • 建立EventChannel,註冊“包名/識別符號”的channel名
    • 通過StreamSubscription#listen註冊listener,其中cancelOnError參數列示遇到錯誤時是否自動結束監聽
    class _MyHomePageState extends State<MyHomePage> {
      static const EventChannel _channel = const EventChannel('com.example.eventchannel/interop');
    
      StreamSubscription _streamSubscription;
      String _platformMessage;
    
      void _enableEventReceiver() {
        _streamSubscription = _channel.receiveBroadcastStream().listen(
            (dynamic event) {
              print('Received event: $event');
              setState(() {
                _platformMessage = event;
              });
            },
            onError: (dynamic error) {
              print('Received error: ${error.message}');
            },
            cancelOnError: true);
      }
    
      void _disableEventReceiver() {
        if (_streamSubscription != null) {
          _streamSubscription.cancel();
          _streamSubscription = null;
        }
      }
    
      @override
      initState() {
        super.initState();
        _enableEventReceiver();
      }
    
      @override
      void dispose() {
        super.dispose();
        _disableEventReceiver();
      }
    複製程式碼
  • native(android)端
    • 通過EventChannel#setStreamHandler註冊Handler實現
    • 初始化完成後,獲取eventSink引用並儲存
    • eventSink傳送事件通知
    • 通知結束時呼叫event#endOfStream,此時onCancel會被呼叫
    • 必要時,可通過evnetSink#error傳送錯誤通知,flutter的StreamSubscription#onError會收到通知
    class MainActivity: FlutterActivity() {
        private lateinit var channel: EventChannel
        var eventSink: EventSink? = null
    
        override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
            GeneratedPluginRegistrant.registerWith(flutterEngine)
    
            channel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.eventchannel/interop")
            channel.setStreamHandler(
                    object : StreamHandler {
                        override fun onListen(arguments: Any?, events: EventSink) {
                            eventSink = events
                            Log.d("Android", "EventChannel onListen called")
                            Handler().postDelayed({
                                eventSink?.success("Android")
                                //eventSink?.endOfStream()
                                //eventSink?.error("error code", "error message","error details")
                            }, 500)
                        }
                        override fun onCancel(arguments: Any?) {
                            Log.w("Android", "EventChannel onCancel called")
                        }
                    })
        }
    }
    複製程式碼

08.BasicMessageChannel流程

  • BasicMessageChannel用於在flutter和native互相傳送訊息,一方給另一方傳送訊息,收到訊息之後給出回覆。

09.BasicMessageChannel基本流程

  • flutter向native傳送訊息
    • [flutter]建立BasicMessageChannel
    • [native]通過BasicMessageChannel#MessageHandler註冊Handler
    • [flutter]通過BasicMessageChannel#send傳送訊息
    • [native]BasicMessageChannel#MessageHandler#onMessage中接收訊息,然後reply
  • 如圖所示
    • image
  • native向flutter傳送訊息
    • 流程也是一樣的,只是將[flutter]與[native]反調
  • 如圖所示
    • image

10.BasicMessageChannel程式碼實現

10.1flutter端

  • flutter需要完成以下工作
    • 建立BasicMessageChannel
    • 通過BasicMessageChannel#send傳送訊息
  • 相對與其他Channel型別的建立,MessageChannel的建立除了channel名以外,還需要指定編碼方式:
    BasicMessageChannel(String name, MessageCodec<T> codec, {BinaryMessenger binaryMessenger})
    複製程式碼
  • 傳送的訊息會以二進位制的形式進行處理,所以要針對不同型別的數進行二進位制編碼
    • 編碼型別 訊息格式
    • BinaryCodec 傳送二進位制訊息時
    • JSONMessageCodec 傳送Json格式訊息時
    • StandardMessageCodec 傳送基本型資料時
    • StringCodec 傳送String型別訊息時
  • 程式碼
    class _MyHomePageState extends State<MyHomePage> {
      static const _channel = BasicMessageChannel('com.ycbjie.android/basic', StringCodec());
    
      String _platformMessage;
    
      void _sendMessage() async {
        final String reply = await _channel.send('Hello World form Dart');
        print(reply);
      }
    
      @override
      initState() {
        super.initState();
    
        // Receive messages from platform
        _channel.setMessageHandler((String message) async {
          print('Received message = $message');
          setState(() => _platformMessage = message);
          return 'Reply from Dart';
        });
    
        // Send message to platform
        _sendMessage();
      }
    複製程式碼

10.2 native(android)端

  • android端完成以下工作:
    • 建立BasicMessageChannel
    • 通過setHandler註冊MessageHandler
    • MessageHandler#onMessage回撥中接收到message後,通過reply進行回覆
  • 程式碼
    class MainActivity: FlutterActivity() {
        override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
            GeneratedPluginRegistrant.registerWith(flutterEngine)
    
            val channel = BasicMessageChannel(
                    flutterEngine.dartExecutor.binaryMessenger,
                    "com.ycbjie.android/basic", StringCodec.INSTANCE)
    
            // Receive messages from Dart
            channel.setMessageHandler { message, reply ->
                Log.d("Android", "Received message = $message")
                reply.reply("Reply from Android")
            }
    
            // Send message to Dart
            Handler().postDelayed({
                channel.send("Hello World from Android") { reply ->
                    Log.d("Android", "$reply")
                }
            }, 500)
        }
    }
    複製程式碼

11.Channel編解碼器說明

11.1 什麼是訊息編解碼器

  • 什麼是訊息編解碼器
    • 在Flutter和平臺間進行相互通訊了,但是收發的資料都是二進位制的,這就需要開發者考慮更多的細節,如位元組順序(大小端)和怎麼表示更高階的訊息型別,如字串,map等。
    • 因此,Flutter 還提供了訊息編解碼器(Codec), 用於高階資料型別(字串,map等)和二進位制資料(byte)之間的轉換,即訊息的序列化和反序列化。
  • 訊息編解碼器種類有哪些
    • MethodCodec:方法傳遞的編解碼器抽象,介面
    • JSONMethodCodec:MethodCodec的實現類,會把資料打包成json結構傳送給dart,類
    • StandardMethodCodec:MethodCodec的實現類,會把資料打包成預設格式傳送給dart,類

11.2 四種訊息編解碼器型別

  • BinaryCodec
    • MessageCodec的實現類,直接傳送二進位制資料
    • BinaryCodec是最為簡單的一種Codec,因為其返回值型別和入參的型別相同,均為二進位制格式(Android中為ByteBuffer,iOS中為NSData)。實際上,BinaryCodec在編解碼過程中什麼都沒做,只是原封不動將二進位制資料訊息返回而已。或許你會因此覺得BinaryCodec沒有意義,但是在某些情況下它非常有用,比如使用BinaryCodec可以使傳遞記憶體資料塊時在編解碼階段免於記憶體拷貝。
  • StringCodec
    • MessageCodec的實現類,負責解碼和編碼String型別的訊息
    • 使用 UTF-8 編碼格式對字串資料進行編解碼,在Android平臺轉換為 java.util.String 型別
  • JSONMessageCodec
    • MessageCodec的實現類,負責解碼和編碼Json型別的訊息
    • JSONMessageCodec用於處理 JSON 資料型別(字串型,數字型,布林型,null,只包含這些型別的陣列,和key為string型別,value為這些型別的map),在編碼過程中,資料會被轉換為JSON字串,然後在使用 UTF-8 格式轉換為位元組型。
  • StandardMessageCodec
    • MessageCodec的實現類,負責解碼和編碼預設型別的訊息
    • StandardMessageCodec 可以認為是 JSONMessageCodec 的升級版,能夠處理的資料型別要比 JSONMessageCodec 更普遍一些,且在處理 int 型資料時,會根據 int 資料的大小來轉為平臺端的32位型別(int)或者是64位型別(long),StandardMessageCodec 也是 Flutter Platform channel 的預設編解碼器

11.3 編碼器的原始碼分析下

  • 首先看下MessageCodec
    abstract class MessageCodec<T> {
    
      ByteData encodeMessage(T message);
    
      T decodeMessage(ByteData message);
    }
    複製程式碼

11.4 看StandardMessageCodec

  • StandardMessageCodec稍微複雜
    • StandardMessageCodec在寫入資料的時候,顯示寫入這個資料的型別值定義,然後在寫入其對應的具體值,什麼意思呢?
  • 檢視一下如何寫入指定型別的值,程式碼如下所示:
      protected void writeValue(ByteArrayOutputStream stream, Object value) {
        if (value == null || value.equals(null)) {
          stream.write(NULL);
        } else if (value == Boolean.TRUE) {
          stream.write(TRUE);
        } else if (value == Boolean.FALSE) {
          stream.write(FALSE);
        } else if (value instanceof Number) {
          if (value instanceof Integer || value instanceof Short || value instanceof Byte) {
            stream.write(INT);
            writeInt(stream, ((Number) value).intValue());
          } else if (value instanceof Long) {
            stream.write(LONG);
            writeLong(stream, (long) value);
          } else if (value instanceof Float || value instanceof Double) {
            stream.write(DOUBLE);
            writeAlignment(stream, 8);
            writeDouble(stream, ((Number) value).doubleValue());
          } else if (value instanceof BigInteger) {
            stream.write(BIGINT);
            writeBytes(stream, ((BigInteger) value).toString(16).getBytes(UTF8));
          } else {
            throw new IllegalArgumentException("Unsupported Number type: " + value.getClass());
          }
        } else if (value instanceof String) {
          stream.write(STRING);
          writeBytes(stream, ((String) value).getBytes(UTF8));
        } else if (value instanceof byte[]) {
          stream.write(BYTE_ARRAY);
          writeBytes(stream, (byte[]) value);
        } else if (value instanceof int[]) {
          stream.write(INT_ARRAY);
          final int[] array = (int[]) value;
          writeSize(stream, array.length);
          writeAlignment(stream, 4);
          for (final int n : array) {
            writeInt(stream, n);
          }
        } else if (value instanceof long[]) {
          stream.write(LONG_ARRAY);
          final long[] array = (long[]) value;
          writeSize(stream, array.length);
          writeAlignment(stream, 8);
          for (final long n : array) {
            writeLong(stream, n);
          }
        } else if (value instanceof double[]) {
          stream.write(DOUBLE_ARRAY);
          final double[] array = (double[]) value;
          writeSize(stream, array.length);
          writeAlignment(stream, 8);
          for (final double d : array) {
            writeDouble(stream, d);
          }
        } else if (value instanceof List) {
          stream.write(LIST);
          final List<?> list = (List) value;
          writeSize(stream, list.size());
          for (final Object o : list) {
            writeValue(stream, o);
          }
        } else if (value instanceof Map) {
          stream.write(MAP);
          final Map<?, ?> map = (Map) value;
          writeSize(stream, map.size());
          for (final Entry<?, ?> entry : map.entrySet()) {
            writeValue(stream, entry.getKey());
            writeValue(stream, entry.getValue());
          }
        } else {
          throw new IllegalArgumentException("Unsupported value: " + value);
        }
      }
    複製程式碼
  • 檢視一下如何讀取指定型別的值,程式碼如下所示:
      protected Object readValueOfType(byte type, ByteBuffer buffer) {
        final Object result;
        switch (type) {
          case NULL:
            result = null;
            break;
          case TRUE:
            result = true;
            break;
          case FALSE:
            result = false;
            break;
          case INT:
            result = buffer.getInt();
            break;
          case LONG:
            result = buffer.getLong();
            break;
          case BIGINT:
            {
              final byte[] hex = readBytes(buffer);
              result = new BigInteger(new String(hex, UTF8), 16);
              break;
            }
          case DOUBLE:
            readAlignment(buffer, 8);
            result = buffer.getDouble();
            break;
          case STRING:
            {
              final byte[] bytes = readBytes(buffer);
              result = new String(bytes, UTF8);
              break;
            }
          case BYTE_ARRAY:
            {
              result = readBytes(buffer);
              break;
            }
          case INT_ARRAY:
            {
              final int length = readSize(buffer);
              final int[] array = new int[length];
              readAlignment(buffer, 4);
              buffer.asIntBuffer().get(array);
              result = array;
              buffer.position(buffer.position() + 4 * length);
              break;
            }
          case LONG_ARRAY:
            {
              final int length = readSize(buffer);
              final long[] array = new long[length];
              readAlignment(buffer, 8);
              buffer.asLongBuffer().get(array);
              result = array;
              buffer.position(buffer.position() + 8 * length);
              break;
            }
          case DOUBLE_ARRAY:
            {
              final int length = readSize(buffer);
              final double[] array = new double[length];
              readAlignment(buffer, 8);
              buffer.asDoubleBuffer().get(array);
              result = array;
              buffer.position(buffer.position() + 8 * length);
              break;
            }
          case LIST:
            {
              final int size = readSize(buffer);
              final List<Object> list = new ArrayList<>(size);
              for (int i = 0; i < size; i++) {
                list.add(readValue(buffer));
              }
              result = list;
              break;
            }
          case MAP:
            {
              final int size = readSize(buffer);
              final Map<Object, Object> map = new HashMap<>();
              for (int i = 0; i < size; i++) {
                map.put(readValue(buffer), readValue(buffer));
              }
              result = map;
              break;
            }
          default:
            throw new IllegalArgumentException("Message corrupted");
        }
        return result;
      }
    複製程式碼

11.5 如何選擇合適編解碼器

  • 編解碼的實現類並不複雜
    • 可以先了解一下這個比較能更好的理解資料傳遞,其實不關java上層使用那種方式,最終傳遞給底層資料都是固定格式,約定統一的資料格式雙方才能識別出來,正常的來說用預設的編解碼格式就可以了。
  • 關於四種解碼器使用場景
    • BinaryCodec
      • 暫未找到使用的場景
    • StringCodec
      • 適用傳送單一的字串資料,資料量單一的情況,比如LifecycleChannel
      public void appIsInactive() {
        Log.v(TAG, "Sending AppLifecycleState.inactive message.");
        channel.send("AppLifecycleState.inactive");
      }
      複製程式碼
    • JSONMessageCodec
      • 適用資料量比較複雜的情況,比如有攜帶多個資料欄位的傳遞,比如KeyEventChannel
      public void keyDown(@NonNull FlutterKeyEvent keyEvent) {
        Map<String, Object> message = new HashMap<>();
        message.put("type", "keydown");
        message.put("keymap", "android");
        encodeKeyEvent(keyEvent, message);
       
        channel.send(message);
      }
      複製程式碼
    • StandardMessageCodec
      • 預設的資料編解碼,絕大多數的情況下使用預設的就可以了。比如:MethodChannel,EventChannel

12.Channel通訊可以子執行緒嗎

12.1 Android傳送通訊資訊

  • 首先看一下Android傳送通訊資訊,主要分析入口是:nativeChannel.invokeMethod("setNum", a , null);
    public void invokeMethod(String method, @Nullable Object arguments, Result callback) {
    messenger.send(
        name,
        codec.encodeMethodCall(new MethodCall(method, arguments)),
        callback == null ? null : new IncomingResultHandler(callback));
    }
    複製程式碼
  • 最終定位找到DartMessenger類的send方法,程式碼如下所示:
    @Override
    public void send(
          @NonNull String channel,
          @Nullable ByteBuffer message,
          @Nullable BinaryMessenger.BinaryReply callback) {
        Log.v(TAG, "Sending message with callback over channel '" + channel + "'");
        int replyId = 0;
        if (callback != null) {
          replyId = nextReplyId++;
          pendingReplies.put(replyId, callback);
        }
        if (message == null) {
          flutterJNI.dispatchEmptyPlatformMessage(channel, replyId);
        } else {
          flutterJNI.dispatchPlatformMessage(channel, message, message.position(), replyId);
        }
    }
    複製程式碼
  • 嘗試一下子執行緒傳送訊息,發現會出現崩潰
    new Thread(new Runnable() {
        @Override
        public void run() {
            nativeChannel.invokeMethod("setNum", a , null);
        }
    }).start();
    複製程式碼
  • 崩潰資訊如下所示
    java.lang.RuntimeException: Methods marked with @UiThread must be executed on the main thread. Current thread: Thread-2574
        at io.flutter.embedding.engine.FlutterJNI.ensureRunningOnMainThread(FlutterJNI.java:992)
        at io.flutter.embedding.engine.FlutterJNI.dispatchPlatformMessage(FlutterJNI.java:736)
        at io.flutter.embedding.engine.dart.DartMessenger.send(DartMessenger.java:72)
        at io.flutter.embedding.engine.dart.DartExecutor$DefaultBinaryMessenger.send(DartExecutor.java:370)
        at io.flutter.plugin.common.MethodChannel.invokeMethod(MethodChannel.java:94)
        at com.ycbjie.ycandroid.channel.MethodChannelActivity.test1000(MethodChannelActivity.java:302)
        at com.ycbjie.ycandroid.channel.MethodChannelActivity.access$000(MethodChannelActivity.java:46)
        at com.ycbjie.ycandroid.channel.MethodChannelActivity$1.run(MethodChannelActivity.java:98)
        at java.lang.Thread.run(Thread.java:818)
    複製程式碼

12.Flutter給NA傳送資料

  • 從method.invokeMethod('android', map);開始分析
      @optionalTypeArgs
      Future<T> _invokeMethod<T>(String method, { bool missingOk, dynamic arguments }) async {
        assert(method != null);
        final ByteData result = await binaryMessenger.send(
          name,
          codec.encodeMethodCall(MethodCall(method, arguments)),
        );
        return codec.decodeEnvelope(result) as T;
      }
    複製程式碼
  • 最後定位到_DefaultBinaryMessenger類中的send方法
      Future<ByteData> _sendPlatformMessage(String channel, ByteData message) {
        final Completer<ByteData> completer = Completer<ByteData>();
        // ui.window is accessed directly instead of using ServicesBinding.instance.window
        // because this method might be invoked before any binding is initialized.
        // This issue was reported in #27541. It is not ideal to statically access
        // ui.window because the Window may be dependency injected elsewhere with
        // a different instance. However, static access at this location seems to be
        // the least bad option.
        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;
      }
    複製程式碼

13.Channel通訊傳遞穩定性

  • channel傳遞資料是否會丟失,如何測試呢?可以模擬,Android給flutter傳送1000條資訊,然後flutter給Android傳送1000條資訊,接下來看一下如何測試:

13.1 Android給flutter傳送資料

  • Android給flutter傳送資料,程式碼如下所示
    int a = 0;
    private void test1000() {
        if (nativeChannel!=null){
            for (int i=0 ; i<1000 ; i++){
                a++;
                Log.i("測試資料test1000 :", a+"");
                nativeChannel.invokeMethod("setNum", a , new MethodChannel.Result() {
                    @SuppressLint("SetTextI18n")
                    @Override
                    public void success(@Nullable Object result) {
                        if (result==null){
                            return;
                        }
                        Log.i("測試資料:",result.toString());
                    }
    
                    @SuppressLint("SetTextI18n")
                    @Override
                    public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
                        Log.i("測試資料異常:",errorMessage);
                    }
    
                    @Override
                    public void notImplemented() {
    
                    }
                });
            }
        }
    }
    複製程式碼
  • flutter接收資料並回傳資料給Android
      //接受na端傳遞過來的方法,並做出響應邏輯處理
      method.setMethodCallHandler(nativeCallHandler);
    
      // 註冊方法,等待被原生通過invokeMethod喚起
      Future<dynamic> nativeCallHandler(MethodCall methodCall) async {
        switch (methodCall.method) {
          case "setNum":
          //獲取引數
            int message = await methodCall.arguments;
            print("原生android傳遞過來的引數為------ $message");
            return "flutter回撥資料:${message.toString()}";
            break;
        }
      }
    複製程式碼

13.2 檢視資料穩定性和及時性

  • Android傳送訊息日誌
    2021-08-26 11:58:03.837 23106-23106/com.ycbjie.ychybrid I/測試資料test1000:: 990
    2021-08-26 11:58:03.837 23106-23106/com.ycbjie.ychybrid I/測試資料test1000:: 991
    2021-08-26 11:58:03.837 23106-23106/com.ycbjie.ychybrid I/測試資料test1000:: 992
    2021-08-26 11:58:03.837 23106-23106/com.ycbjie.ychybrid I/測試資料test1000:: 993
    2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/測試資料test1000:: 994
    2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/測試資料test1000:: 995
    2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/測試資料test1000:: 996
    2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/測試資料test1000:: 997
    2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/測試資料test1000:: 998
    2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/測試資料test1000:: 999
    2021-08-26 11:58:03.838 23106-23106/com.ycbjie.ychybrid I/測試資料test1000:: 1000
    複製程式碼
  • flutter接收Android傳送資料
    2021-08-26 11:52:39.708 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android傳遞過來的引數為------ 992
    2021-08-26 11:52:39.709 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android傳遞過來的引數為------ 993
    2021-08-26 11:52:39.709 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android傳遞過來的引數為------ 994
    2021-08-26 11:52:39.709 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android傳遞過來的引數為------ 995
    2021-08-26 11:52:39.709 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android傳遞過來的引數為------ 996
    2021-08-26 11:52:39.710 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android傳遞過來的引數為------ 997
    2021-08-26 11:52:39.710 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android傳遞過來的引數為------ 998
    2021-08-26 11:52:39.710 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android傳遞過來的引數為------ 999
    2021-08-26 11:52:39.710 23106-23627/com.ycbjie.ychybrid I/flutter: 原生android傳遞過來的引數為------ 1000
    複製程式碼
  • flutter收到訊息後,回撥給Android資料。Android監聽回撥資料,列印日誌如下
    2021-08-26 11:58:03.964 23106-23106/com.ycbjie.ychybrid I/測試資料:: flutter回撥資料:600
    2021-08-26 11:58:03.964 23106-23106/com.ycbjie.ychybrid I/測試資料:: flutter回撥資料:601
    2021-08-26 11:58:03.964 23106-23106/com.ycbjie.ychybrid I/測試資料:: flutter回撥資料:602
    2021-08-26 11:58:03.965 23106-23106/com.ycbjie.ychybrid I/測試資料:: flutter回撥資料:603
    2021-08-26 11:58:03.965 23106-23106/com.ycbjie.ychybrid I/測試資料:: flutter回撥資料:604
    2021-08-26 11:58:03.965 23106-23106/com.ycbjie.ychybrid I/測試資料:: flutter回撥資料:605
    2021-08-26 11:58:03.965 23106-23106/com.ycbjie.ychybrid I/測試資料:: flutter回撥資料:606
    2021-08-26 11:58:03.966 23106-23106/com.ycbjie.ychybrid I/測試資料:: flutter回撥資料:607
    複製程式碼
  • 然後再看一波列印日誌,如下所示
    2021-08-26 12:07:09.158 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 1
    2021-08-26 12:07:09.237 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:1
    2021-08-26 12:07:09.240 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 2
    2021-08-26 12:07:09.241 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 3
    2021-08-26 12:07:09.241 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:2
    2021-08-26 12:07:09.241 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:3
    2021-08-26 12:07:09.241 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 4
    2021-08-26 12:07:09.241 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:4
    2021-08-26 12:07:09.241 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 5
    2021-08-26 12:07:09.241 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:5
    2021-08-26 12:07:09.242 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 6
    2021-08-26 12:07:09.242 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:6
    2021-08-26 12:07:09.242 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 7
    2021-08-26 12:07:09.242 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:7
    
    2021-08-26 12:07:09.272 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 131
    2021-08-26 12:07:09.273 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:131
    2021-08-26 12:07:09.273 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 132
    2021-08-26 12:07:09.273 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:132
    2021-08-26 12:07:09.273 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 133
    2021-08-26 12:07:09.273 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:133
    2021-08-26 12:07:09.273 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 134
    2021-08-26 12:07:09.273 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:134
    2021-08-26 12:07:09.273 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 135
    2021-08-26 12:07:09.274 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:135
    2021-08-26 12:07:09.274 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 136
    2021-08-26 12:07:09.274 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:136
    2021-08-26 12:07:09.274 24237-24990/com.ycbjie.ychybrid I/flutter: 測試資料,flutter接收NA資料: 137
    2021-08-26 12:07:09.274 24237-24237/com.ycbjie.ychybrid I/測試資料,NA接收flutter回撥:: flutter回撥資料:137
    複製程式碼
  • 因此檢視日誌可以得知,傳遞資料保證了資料的時效性,傳送訊息和接收訊息是一一對應。並沒有失敗的情況,因此傳遞資料是穩定的。
  • 重點說明,有小夥伴有疑惑,你這遍歷1000次,每次傳遞都是int值,那實際開發中可能傳遞大json,資料量大的情況會怎樣,這個下面會說到……

14.onActivityResult如何實現

  • 先說一個場景
    • 在開發中我們經常會遇到關閉當前頁面的同時返回給上一個頁面資料的場景,在Android中是通過startActivityForResult和onActivityResult()實現的。
    • 而純Flutter頁面之間可以通過在Navigator.of(context).pop()方法中新增引數來實現,那麼對於Flutter頁面和Android原生頁面之間如何在返回上一頁時傳遞資料呢,通過MethodChannel就可以實現。

14.1 Flutter頁面返回Android原生頁面

  • 在Flutter端呼叫原生的返回方法就可以了,首先在Flutter頁面新增一個按鈕,點選按鈕返回原生頁面,程式碼如下:
    new Padding(
        padding: const EdgeInsets.only(
            left: 10.0, top: 10.0, right: 10.0),
        child: new RaisedButton(
            textColor: Colors.black,
            child: new Text('返回上一介面,並攜帶資料'),
            onPressed: () {
                Map<String, dynamic> map = {'message': '我從Flutter頁面回來了'};
                String result = await method.invokeMethod('goBackWithResult', map);
            }),
      ),
    複製程式碼
  • Android端依然是通過判斷methodCall.method的值來執行指定的程式碼,通過methodCall.argument()獲取Flutter傳遞的引數。
    nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
        @Override
        public void onMethodCall(@NonNull MethodCall methodCall, @NonNull MethodChannel.Result result) {
            if ("goBackWithResult".equals(methodCall.method)) {
                // 返回上一頁,攜帶資料
                Intent backIntent = new Intent();
                backIntent.putExtra("message", (String) methodCall.argument("message"));
                setResult(RESULT_OK, backIntent);
                finish();
            }
        }
    });
    複製程式碼

14.2 Android原生頁面返回Flutter頁面

  • Android原生頁面返回Flutter頁面
    • 這種情況需要原生來呼叫Flutter程式碼,和Flutter呼叫原生方法的步驟是一樣的。首先觸發flutter頁面按鈕,從flutter跳轉na頁面,然後觸發na頁面返回操作,返回到Flutter頁面,並傳遞資料。
  • 首先是flutter頁面觸發跳轉到na頁面的程式碼操作邏輯,程式碼如下所示
    //flutter
      new Padding(
        padding: const EdgeInsets.only(left: 10.0, top: 10.0, right: 10.0),
        child: new RaisedButton(
            textColor: Colors.black,
            child: new Text('跳轉到原生逗比介面,回撥結果:$_methodResult1'),
            onPressed: () {
              _jumpToNative();
            }),
      ),
    
    //na,注意na接收到flutter指令後,na是呼叫startActivityForResult操作跳轉到na的新頁面
    nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
        @Override
        public void onMethodCall(@NonNull MethodCall methodCall, @NonNull MethodChannel.Result result) {
            if ("doubi".equals(methodCall.method)) {
                //接收來自flutter的指令
                //跳轉到指定Activity
                Intent intent = new Intent(MethodChannelActivity.this, MethodResultActivity.class);
                startActivityForResult(intent,RESULT_OK2);
                //返回給flutter的引數
                result.success("Na收到指令");
            }
        }
    });
    複製程式碼
  • 然後接下來的一步是,從NA返回到flutter頁面,然後再去呼叫flutter方法。具體操作程式碼如下所示
    //na flutter觸發開啟na的新的頁面
    public class MethodResultActivity extends AppCompatActivity {
    
        @SuppressLint("SetTextI18n")
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_android);
            TextView tv = findViewById(R.id.tv);
            tv.setText("flutter頁面開啟NA頁面,測試Android原生頁面返回Flutter頁面");
            tv.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Intent intent = new Intent();
                    intent.putExtra("message", "我從原生頁面回來了");
                    setResult(RESULT_OK2, intent);
                    finish();
                }
            });
        }
    }
    
    // na flutter承載容器的na的原生頁面
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (data != null && resultCode==RESULT_OK2) {
            // MethodResultActivity返回的資料
            String message = data.getStringExtra("message");
            Map<String, Object> result = new HashMap<>();
            result.put("message", message);
            // 呼叫Flutter端定義的方法
            nativeChannel.invokeMethod("onActivityResult", result, new MethodChannel.Result() {
                @SuppressLint("SetTextI18n")
                @Override
                public void success(@Nullable Object result) {
                    tvContent.setText("測試內容2:"+result);
                }
    
                @SuppressLint("SetTextI18n")
                @Override
                public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
                    tvContent.setText("測試內容:flutter傳遞給na資料傳遞錯誤2");
                }
    
                @Override
                public void notImplemented() {
    
                }
            });
        }
    }
    
    //flutter
      Future<dynamic> handler(MethodCall call) async {
        switch (call.method) {
          case 'onActivityResult':
            // 獲取原生頁面傳遞的引數
            print(call.arguments['message']);
            return "你好,這個是從flutter傳遞過來的資料";
        }
      }
    
      flutterChannel.setMethodCallHandler(handler);
    複製程式碼

推薦

相關文章