底层原理 - 深入探索Platform Channel

Flutter 提供了完善的 Platform Channel 机制供 Native 与 Flutter 端进行相互通信,本节由浅入深,详细探索 Platform Channel 的实现细节。

怎么用

首先我们简单介绍下 Platform Channel 的使用姿势,Flutter提供了三种Channel的方式:

  • BaseMessageChannel,消息的传递,用于数据的传递。
  • MethodChannel,方法的传递,用于方法调用,传递参数并返回执行结果。
  • EventChannel,事件的传递,用于Native端对Flutter端高频的通知触发。

MethodChannel - Android

构造函数,通常messenger传递FlutterNativeView实现的DartExecutor, name是Channel匹配的key值,codec是消息传递的编解码,默认是StandardMethodCodec。

1
MethodChannel(BinaryMessenger messenger, String name, MethodCodec codec)

调用Flutter端方法,参数依次是方法名、参数以及返回值的回调。

1
invokeMethod(String method, Object arguments, MethodChannel.Result callback)

设置方法被调用的处理器,Flutter调用原生代码会调用handler的onMethodCall方法。

1
setMethodCallHandler(MethodChannel.MethodCallHandler handler)

MethodChannel - iOS

构造函数

1
2
3
- (instancetype)initWithName:(NSString*)name
binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger
codec:(NSObject<FlutterMessageCodec>*)codec;

调用Flutter方法

1
2
3
- (void)invokeMethod:(NSString*)method
arguments:(id _Nullable)arguments
result:(FlutterResult _Nullable)callback

设置方法回调

1
- (void)setMethodCallHandler:(FlutterMethodCallHandler _Nullable)handler;

MethodChannel - Flutter

构造函数

1
- MethodChannel(this.name, [this.codec = const StandardMethodCodec()])

调用Native方法

1
- invokeMethod(String method, [dynamic arguments])

设置方法回调

1
- setMethodCallHandler(Future<dynamic> handler(MethodCall call))

BaseMessageChannel、EventChannel和MethodChannel的使用方式十分类似,这里就不一一赘述了。

怎么实现的

Platform Channel 以 engine 为媒介,在 native 端或者 dart 端进行消息的编码,经过 engine 传递,在另一端进行消息的解码,完成整个消息的通信。这个过程涉及到 messager (信使)、
codec(编解码器)以及 handler(处理器),下面将分别介绍。

messager

messager 在整个消息传递过程中,作为信使的角色,实现了完整的消息传递。我们看一下 Android 端 BinaryMessenger 的代码,iOS 和 Dart 部分 API 高度一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface BinaryMessenger {

void send(String channel, ByteBuffer message);

void send(String channel, ByteBuffer message, BinaryReply callback);

void setMessageHandler(String channel, BinaryMessageHandler handler);

interface BinaryMessageHandler {

void onMessage(ByteBuffer message, BinaryReply reply);
}

interface BinaryReply {

void reply(ByteBuffer reply);
}
}

BinaryMessenger 提供了两个 send 方法,可选择是否回调消息的处理结果,提供了 setMessageHandler 用以处理 Dart 端 Channel 调用的消息处理。BinaryMessenger 的一个具体实现在 DartMessenger,维护了 messageHandlers 和 pendingReplies 两个 HashMap 分别处理 Dart 端消息请求的响应以及自己发送消息之后的回调。具体的消息传递的完成流程我们等会再讲,先分析一下 codec(编解码器)和 handler(处理器)的实现细节。

codec

让消息在 dart、engine 以及平台( Android / iOS )之间自由传递,需要将消息在传递前编码为平台无关的二进制数据,在接收后解码为原始的数据格式。Flutter 提供了两种 codec , BaseMessageChannel 使用了 MessageCodec , EventChannel 和 MethodChannel 使用了 MethodCodec。

MessageCodec 提供了 encodeMessage 和 decodeMessage 两个接口,以 Android 为例,它的代码实现如下:

1
2
3
4
5
6
public interface MessageCodec<T> {

ByteBuffer encodeMessage(T message);

T decodeMessage(ByteBuffer message);
}

Flutter 官方默认已经实现的 MessageCodec 有 StandardMessageCodec、BinaryCodec、StringCodec、JSONMessageCodec 。默认已经实现的 MethodCodec 有 StandardMethodCodec、JSONMethodCodec。

Codec Desc
MessageCodec StandardMessageCodec 支持基础数据格式与二进制之间的转换,包括 Boolean 、Number、String、Byte、List、Map、数组等。
BinaryCodec 支持二进制数据的直接传递,实际上是没有做任何编解码,可以通过它做大内存块的数据传递。
StringCodec 支持字符串(UTF-8)与二进制之间的转换。
JSONMessageCodec 支持把数据转换为JSON,再调用 StringCodec 的编解码。
MethodCodec StandardMethodCodec 调用 StandardMessageCodec 传递方法的名字和参数。
JSONMethodCodec 调用 JSONMessageCodec 传递方法的名字和参数。
消息的编解码方式可以根据自己的需要从系统提供的 Codec 中选取,也可以完全自定义自己的 Codec 满足实际的业务需求。这里想实现一种基于 Protocol Buffers 协议的编解码器,作为一种简单的学习锻炼,后续会发布在 [Github](https://github.com/flutterboom/flutter-triple-sample) 上供大家参考。

Handler

Flutter 提供了 Handler 作为消息的处理器,用来处理接收方获取的消息。针对 BaseMessageChannel、MethodChannel 和 EventChannel 三种 Channel 分别提供了 IncomingMessageHandler、IncomingMethodCallHandler 和 IncomingStreamRequestHandler 三种 Handler,实际上是分别包装了 MessageHandler 、MethodCallHandler 和 StreamHandler 来处理对方发送过来的消息。

MessageHandler

1
2
3
4
5
6
7
8
9
public interface MessageHandler<T> {

void onMessage(T message, Reply<T> reply);
}

public interface Reply<T> {

void reply(T reply);
}

onMessage 接收获取的 message ,并提供回调 reply 。

MethodCallHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface MethodCallHandler {

void onMethodCall(MethodCall call, Result result);
}

public interface Result {

void success(@Nullable Object result);

void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails);

void notImplemented();
}

onMethodCall 响应调用的函数,并提供函数的返回值 Result。

StreamHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface StreamHandler {

void onListen(Object arguments, EventSink events);

void onCancel(Object arguments);
}

public interface EventSink {

void success(Object event);

void error(String errorCode, String errorMessage, Object errorDetails);

void endOfStream();
}

onListen 开启监听并发送数据,onCancel 取消监听。

消息传递过程

分析 Platform Channel 一个消息的完整调用路径:

native -> dart

从 DartMessenger 的 send 方法开始,
Java 通过 JNI 、iOS 直接调用 C/C++代码,
把消息通过 dispatchPlatformMessage 方法传递到 engine 层的 PlatformView 中,
然后分别经过 engine 、RuntimeController 最后调用 window 的 dispatchPlatformMessage 方法。
window 会自增一个 response_id 并创建一个 response 保存在 pending_responses_ 队列中,通过 tonic 库中的 DartInvokeField 方法调用 Dart 端的 _dispatchPlatformMessage,
完成了一个消息从 native 到 dart 的传递,
dart 端调用 window 的 onPlatformMessage 方法,
通过 channel name 匹配出已经注册好的 handler ,
执行后 通过 window 的 respondToPlatformMessage 方法回调到 engine 层的 RespondToPlatformMessage 方法。
engine 根据 response_id 在 pending_responses
取出对应的 response 执行 Complete 方法把结果返回给 Native 端,
Native 端在 DartMessenger 中的 handlePlatformMessageResponse 方法中处理具体返回的结果,完成了一整个的消息传递过程。

dart -> native

完整的过程跟 native 到 dart 的逻辑基本一致,除了一些函数调用上的差别,文字描述比较费力,这边是简单的做了一个图比较直观的看到整个消息的具体流向。

Flutter Support Native

在整个 Platform Channel 的实现细节梳理的过程中,我们不仅学习了一个数据通信的完整架构,同时也看到了大量的多种语言之间的相互调用。消息可以在不同的语言中自由的流转,主要是各种语言都实现了到 C/C++的函数调用的框架,Java 和 OC 已经有很成熟的支持 Native 的方案,Dart 也提供对 C/C++ 代码的调用的技术,具体的用法如下:

Dart 调用 C/C++ 代码

Dart 对 C/C++ 的支持目前还并不完善,社区中吐槽 Dart 对 Native 支持进展缓慢的帖子很多,主要的嘈点是目前官方对于 Native 的支持文章 Native extensions for the standalone Dart VM 还是 12 年 5 月份撰写的, 笔者也是根据这篇文章的指南调通了 Dart 对 C++ 的调用。在C++中实现了一个简单的加法运算,打包成 lib 提供给 Dart 调用。部分主要代码如下,完整工程可以在 Github 上查看,欢迎 Star 。

native_plus.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//第一次调用给定名称的本地函数时,将本地函数的 Dart 名称解析为 C 函数指针。
Dart_NativeFunction ResolveName(Dart_Handle name,
int argc,
bool* auto_setup_scope);

//在 lib 被加载时被调用。
DART_EXPORT Dart_Handle native_plus_Init(Dart_Handle parent_library) ;

// 实现了一个加法运算
void NativePlus(Dart_NativeArguments args) {
Dart_EnterScope();
int64_t result = 0;
Dart_Handle plus_a = HandleError(Dart_GetNativeArgument(args, 0));
Dart_Handle plus_b = HandleError(Dart_GetNativeArgument(args, 1));
if (Dart_IsInteger(plus_a) && Dart_IsInteger(plus_b)) {
int64_t a,b;
HandleError(Dart_IntegerToInt64(plus_a, &a));
HandleError(Dart_IntegerToInt64(plus_b, &b));
result = a + b;
}
Dart_SetReturnValue(args, HandleError(Dart_NewInteger(result)));
Dart_ExitScope();
}

//注册的方法列表
FunctionLookup function_list[] = {
{"NativePlus", NativePlus},
{NULL, NULL}};

C++代码可以根据不同平台 build 对应的链接库,我这边测试了在 MacOS上的导出,具体步骤如下:

使用 Xcode 创建本地扩展 dylib 项目:
1、选择新建类型:File -> New -> Project -> macOS -> Library
2、填写项目选项:
- Project Name:native_plus
- Framework:None(Plain C/C++ Library)
- Type:Dynamic
3、添加 native_plus.cc 文件到项目中。
4、在 Build Settings 中进行以下更改,在对话框中选择 Build 选项卡和 All Configurations :
- 在 Linking 部分, Other Linker Flags 项中,增加 -undefined dynamic_lookup 。
- 在 Search Paths 部分, Header Search Paths 项中,增加 dart_api.h 文件路径,在文件位于已下载的 SDK 或 Dart 仓库中。
- 在 Preprocessing 部分, Preprocessor Macros 项中,增加 DART_SHARED_LIB=1 。
5、Build。

test_native_plus.dart

1
2
3
4
5
6
7
//导入本地库,将 Build 生成的 libnative_plus.dylib 放置在同目录下
import 'dart-ext:native_plus';
//用 Native 修饰本地库的函数
int nativePlus(int a, int b) native "NativePlus";
void main() {
print(nativePlus(3, 5));
}

另外还可以使用 [C & C++ interop using FFI] (https://dart.dev/server/c-interop) 对已经开发好的 C/C++ 链接库直接调用。

Tonic

Dart 对 Native 的支持目前还不是很完善,Flutter 团队目前开发的 Tonic 库很好的去支持 Dart 与 C/C++ 代码的相互调用。Tonic 的代码仓库地址在这里
A collection of C++ utilities for working with the DartVM API. 通过 Tonic 可以完整实现 Dart 与 C++ 之间的代码交互。

总结

本文详细梳理了 Platform Channel 完整的实现细节,最大的收获是对数据通信整体架构的设计和 Dart 与 C/C++ 之间代码的相互调用实现。行文比较仓促,作为一个 Android Coder ,部分案例也是基于 Android 的角度分析代码,可能对于 iOS 同学有一些不太友好,还望海涵。文中涉及到的任何内容,欢迎大家留言讨论。