Flutter 安卓 Platform 與 Dart 端訊息通訊方式 Channel 原始碼解析

工匠若水發表於2021-08-25

這是我參與8月更文挑戰的第7天,活動詳情檢視: 8月更文挑戰

Flutter 系列文章連載~

背景

本系列前面已經分析了 Flutter 的很多知識,這一篇我們來看下 Flutter 平臺通訊相關原理。Flutter 官方提供三種 Platform 與 Dart 端訊息通訊方式,他們分別是 MethodChannel、BasicMessageChannel、EventChannel,本文會繼續延續前面系列對他們進行一個深度解析,原始碼依賴 Flutter 2.2.3 版本,Platform 選取熟悉的 Android 平臺實現。

對於 MethodChannel、BasicMessageChannel、EventChannel 三種官方訊息通訊方式來說,他們都是全雙工通訊,所以基於他們我們基本可以實現 Platform 與 Dart 的各種通訊能力。他們各自適用場景如下:

  • MethodChanel:用於傳遞方法呼叫,MethodCallHandler 最終必須在 UI 執行緒通過result.success(x)方法返回結果,返回前自己可以非同步新起執行緒做任意耗時操作。
  • BasicMessageChannel:用於傳遞字串和半結構化的訊息。
  • EventChannel:用於資料流的傳送。

基礎使用技巧

這些通訊方式的基礎用法我們這裡就不再解釋了,這裡重點說下技巧,在編寫 Platform 程式碼時有兩個特別注意的點:

  • 對於 Mac 使用者,如果你要通過 Mac 的 Android Studio 開啟 Flutter 自動建立的.android 專案,記得吊起訪達後通過快捷鍵Command + Shift + '.'顯示隱藏目錄即可。
  • 修改 Platform 端的程式碼後如果執行沒生效則請關閉 app 重新編譯,因為熱部署對 Platform 無效。

日常工作中我們使用最多的是 MethodChannel,但是他卻不是型別安全的,為了解決這個問題官方推薦使用 Pigeon 包作為 MethodChannel 的替代品,它將生成以結構化型別安全方式傳送訊息的程式碼,但是他目前還不穩定。

更多關於他們基礎使用案例參見官方文件flutter.dev/docs/develo…

訊息收發傳遞原始碼分析

下面原始碼分析我們依舊秉承以使用方式為入口,分 Platform、Engine、Dart 層各自展開。

Platform 端收發實現流程

在進行 Platform 端原始碼分析前請先記住下面這幅圖,如下 Platform 的 Java 側原始碼基於此圖展開分析。 在這裡插入圖片描述 我們先分別看下 MethodChannel、BasicMessageChannel、EventChannel 在 Platform 端的構造成員原始碼:

public class MethodChannel {
  private final BinaryMessenger messenger;
  private final String name;
  private final MethodCodec codec;
  //......
  private final class IncomingMethodCallHandler implements BinaryMessageHandler {
    private final MethodCallHandler handler;
  }
}

public final class BasicMessageChannel<T> {
  @NonNull private final BinaryMessenger messenger;
  @NonNull private final String name;
  @NonNull private final MessageCodec<T> codec;
  //......
  private final class IncomingMessageHandler implements BinaryMessageHandler {
    private final MessageHandler<T> handler;
  }
}

public final class EventChannel {
  private final BinaryMessenger messenger;
  private final String name;
  private final MethodCodec codec;
  //......
  private final class IncomingStreamRequestHandler implements BinaryMessageHandler {
    private final StreamHandler handler;
  }
}
複製程式碼

可以看到,Platform 端無論哪種方式,他們都有三種重要的成員,分別是:

  • name:String 型別,唯一識別符號代表 Channel 的名字,因為一個 Flutter 應用中存在多個 Channel,每個 Channel 在建立時必須指定一個獨一無二的 name 作為標識,這點我們在前面系列原始碼分析中已經見過很多框架實現自己的 name 定義了。
  • messager:BinaryMessenger 型別,充當信使郵遞員角色,訊息的傳送與接收工具人。
  • codec:MethodCodec 或MessageCodec<T>型別,充當訊息的編解碼器。

所以,MethodChannel、BasicMessageChannel、EventChannel 的 Java 端原始碼其實自身是沒有什麼的,重點都在 BinaryMessenger,我們就不貼原始碼了(比較簡單),整個 Java 端收發的流程(以 MethodChannel 為例)大致如下: 在這裡插入圖片描述 上面流程中的 DartMessenger 就是 BinaryMessenger 的實現,也就是 Platform 端與 Dart 端通訊的信使,這一層通訊使用的訊息格式為二進位制格式資料(ByteBuffer)。

可以看到,當我們初始化一個 MethodChannel 例項並註冊處理訊息的回撥 Handler 時會生成一個對應的 BinaryMessageHandler 例項,然後這個例項被放進信使的一個 Map 中,key 就是我們 Channel 的 name,當 Dart 端傳送訊息到 DartMessenger 信使時,信使會根據 name 找到對應 BinaryMessageHandler 呼叫,BinaryMessageHandler 中通過呼叫 MethodCodec 解碼器進行二進位制解碼(預設 StandardMethodCodec 解碼對應平臺資料型別),接著我們就可以使用解碼後的回撥響應。

當我們通過 Platform 呼叫 Dart 端方法時,也是先通過 MethodCodec 編碼器對平臺資料型別進行編碼成二進位制格式資料(ByteBuffer),然後通過 DartMessenger 信使呼叫 FlutterJNI 交給 Flutter Engine 呼叫 Dart 端對應實現。

Dart Framework 端收發實現流程

在進行 Dart 端原始碼分析前請先記住下面這幅圖,如下原始碼基於此圖展開分析。 在這裡插入圖片描述 是不是 Dart 端的像極了 Platform 端收發實現流程圖,同理我們看下 Dart Framework 端對應 Channel 實現類成員:

class MethodChannel {
  final String name;
  final MethodCodec codec;
  final BinaryMessenger? _binaryMessenger;
  //......
}

class BasicMessageChannel<T> {
  final String name;
  final MessageCodec<T> codec;
  final BinaryMessenger? _binaryMessenger;
  //......
}

class EventChannel {
  final String name;
  final MethodCodec codec;
  final BinaryMessenger? _binaryMessenger;
  //......
}
複製程式碼

可以看到,Dart 端無論哪種方式,他們也都有三種重要的成員,分別是 name、codec、_binaryMessenger,而且他們的職責和 Platform 端完全一樣。也就是說 Dart 端就是 Platform 端的一個映象實現而已,框架設計到原理步驟完全一致,區別僅僅是實現語言的不同。

所以,整個 Dart 端收發的流程(以 MethodChannel 為例)大致如下: 在這裡插入圖片描述 有了上圖不用再貼程式碼了吧,和 Platform 端如出一轍,只是換了個語言實現而已。

Flutter Engine C++ 收發實現流程

上面 Platform 與 Dart 端的通訊都分析完畢了,現在就差中間粘合層的 Engine 呼叫了,Engine 的分析我們依然依據呼叫順序為主線檢視。通過上面分析我們可以得到如下資訊:

  • Platform 呼叫 Dart 時 Java 最終呼叫了 FlutterJNI 的private native void nativeDispatchPlatformMessage(long nativeShellHolderId, String channel, ByteBuffer message, int position, int responseId)方法傳遞到 Engine,Engine 最終呼叫了 Dart Framework 中hooks.dartvoid _dispatchPlatformMessage(String name, ByteData? data, int responseId)方法,然後層層傳遞到我們的 Widget 中的 MethodChannel。
  • Dart 呼叫 Platform 時 Dart 最終呼叫了 PlatformDispatcher 的String? _sendPlatformMessage(String name, PlatformMessageResponseCallback? callback, ByteData? data)方法(即native 'PlatformConfiguration_sendPlatformMessage')傳遞到 Engine,Engine 最終呼叫了 Platform 端 FlutterJNI 的public void handlePlatformMessage(final String channel, byte[] message, final int replyId)方法,然後層層傳遞到我們的 MethodChannel 設定的 MethodCallHandler 回撥的 onMethodCall 方法中。

因此我們順著這兩端的入口分析原始碼可以得到如下呼叫順序圖: 在這裡插入圖片描述 上圖對應的 Engine C++ 程式碼呼叫及類所屬檔案都已經交代的很詳細了,原始碼就不再貼片段了,相信你順著這條鏈也能根懂原始碼。特別注意上面 Engine 在負責轉發訊息時的黃色 TaskRunner,其中 PlatformTaskRunner 就是平臺層的主執行緒(安卓 UI 執行緒),所以 Channel 在安卓端的回撥被切換執行在 UI 執行緒中,Channel 在 Dart 端的回撥被切換執行在 Flutter Dart UI 執行緒(即 UITaskRunner 中)。

訊息編解碼原始碼分析

搞懂了 Channel 的收發流程,你可能對上面的編解碼器還有疑惑,他是怎麼做到 Dart 與不同平臺語言型別間轉換的? 我們都知道,一般跨語言或平臺傳輸物件首選方案是通過 json 或 xml 格式,而 Flutter 也不例外,譬如他也提供了 JSONMessageCodec、JSONMethodCodec 等編解碼器,同樣也是將二進位制位元組流轉換為 json 進行處理,像極了我們 http 請求中位元組流轉字串轉 json 轉物件的機制,這樣就抹平了平臺差異。 對於 Flutter 的預設實現來說,最值得關注的就是 StandardMethodCodec 和 StandardMessageCodec,由於 StandardMethodCodec 是對 StandardMessageCodec 的一個包裝,所以本質我們研究下 StandardMessageCodec 即可。如下:

public class StandardMessageCodec implements MessageCodec<Object> {
  //把Java物件型別Object轉為位元組流ByteBuffer
  @Override
  public ByteBuffer encodeMessage(Object message) {
    //......
    final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream();
    writeValue(stream, message);
    final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size());
    buffer.put(stream.buffer(), 0, stream.size());
    return buffer;
  }
  //把位元組流ByteBuffer轉為Java物件型別Object
  @Override
  public Object decodeMessage(ByteBuffer message) {
    //......
    message.order(ByteOrder.nativeOrder());
    final Object value = readValue(message);
    //......
    return value;
  }
  //......
}
複製程式碼

可以看到,在 Platform 端(Android Java)StandardMessageCodec 的作用就是位元組流轉 Java 物件型別,Java 物件型別轉位元組流,核心本質是 StandardMessageCodec 的 readValue 和 writeValue 方法,如下:

protected void writeValue(ByteArrayOutputStream stream, Object value) {
  if (value == null || value.equals(null)) {
    stream.write(NULL);
  } else if (value instanceof Boolean) {
    stream.write(((Boolean) value).booleanValue() ? TRUE : 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);
  }
}
複製程式碼

不用解釋了吧,這不就是列舉一堆支援的型別然後按照位元組位數擷取轉換的操作,所以這也就是為什麼官方文件中明確列舉了 Channel 支援的資料型別,如下: 在這裡插入圖片描述 上面是 Platform 端物件型別與二進位制之間的轉換原理,對於 Dart 端我想你應該也就懂了,無非也是類似操作,不再贅述。

總結

上面全程都以 MethodChannel 進行了原始碼分析,其他 Channel 我們沒有進行分析,但其實本質都一樣,僅僅是一種封裝而已,希望你有需求的時候知道怎麼舉一反三。

相關文章