底层原理 - 深入探索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 同学有一些不太友好,还望海涵。文中涉及到的任何内容,欢迎大家留言讨论。

Flutter接入现有Android工程踩坑之旅

把Flutter作为一个模块接入到现有的Android工程,Flutter有官方推荐方案 Add Flutter to existing apps,通过这样的工程配置,可以在debug支持HotReload,也可以输出Release包供发布。不过在使用过程中有一些需要调整的地方,特此记录希望对大家能有借鉴意义。

工程目录调整

flutter create -t module

命令会创建一个支持Flutter的Android Library,其中Android Library的目录位于Flutter工程的隐藏目录 .android/flutter 中, 一般情况下,我们会把Flutter代码和Android代码放在两个git仓库,通过submodule的方式进行依赖,可以把这个Library的代码copy到你的工程目录下,同时修改flutter的资源目录到你自己的相对路径下:

flutter {
source ‘ your own flutter project directory
}

另外需要Copy include_flutter.groovy 这个文件到你的工程目录下,修改相应的目录添加对于Library的依赖。

armeabi支持

Flutter官方只提供了四种CPU架构的SO库:armeabi-v7a、arm64-v8a、x86和x86-64。但是目前我们对接的两个项目组分别是只支持armeabi和只支持armeabi-v7a,所以需要对官方的jar包进行改造。官方SDK提供的jar包路径在 $flutterRoot/bin/cache/artifacts/engine中,复制这几个目录下的armeabi-v7a中的so到armeabi路径下:

  • android-arm
  • android-arm-dynamic-profile
  • android-arm-dynamic-release
  • android-arm-profile
  • android-arm-release

可以通过如下脚本实现:

1
2
3
4
unzip flutter.jar lib/armeabi-v7a/libflutter.so
mkdir lib/armeabi
cp lib/armeabi-v7a/libflutter.so lib/armeabi/libflutter.so
zip flutter.jar lib/armeabi-v7a/libflutter.so lib/armeabi/libflutter.so

Library中的build.gradle中有一段是通过本地的一个gradle文件添加flutter.jar的依赖:

1
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

我们把flutter.gradle文件以及我们刚才处理的flutter.jar文件Copy到自己的工程路径下,我自己的工程路径配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
project
│ app
└───flutter
│ │ build.gradle
│ │ flutter.gradle
│ │ include_flutter.groovy
│ └───flutter-jars
│ └───android-arm
| | flutter.jar
│ └───android-arm-dynamic-profile
| | flutter.jar
│ └───android-arm-dynamic-release
| | flutter.jar
| └───android-arm-profile
| | flutter.jar
| └───aandroid-arm-release
| | flutter.jar
│ settings.gradle

将flutter.gradle 中jar文件的路径改为本地工程:

1
2
3
4
5
debugFlutterJar = new File('flutter-jars/debug/flutter.jar')
profileFlutterJar = new File('flutter-jars/profile/flutter.jar')
releaseFlutterJar = new File('flutter-jars/release/flutter.jar')
dynamicProfileFlutterJar = new File('flutter-jars/dynamicProfile/flutter.jar')
dynamicReleaseFlutterJar = new File('flutter-jars/dynamicRelease/flutter.jar')

这样打出的AAR就能同时支持两种架构。

打包AAR问题

按照上面的配置,可以在工程中打出支持Debug HotReload和Release的包,不过在输出AAR给别的业务模块使用时会报一个崩溃:

must be able to initialize the ICU context.

这是Android Gradle Plugin 3.+ 版本的一个bug,它会丢弃flutter.jar 中的 /assets/flutter_shared/icudtl.dat文件到AAR中,导致运行时找不到这个文件崩溃,在2.+版本中发现没有这个问题,所以需要使用Android Gradle Plugin 2.+版本,我这边测试2.2.3版本是ok的。但是Android Gradle 2的版本有一个由来已久的问题就是Library不能获取project一致 的BuildType,Library默认只发布Release的AAR。这是因为Android中默认指定了发布type:

1
2
private String defaultPublishConfig = "release";
private boolean publishNonDefault = false;

默认Release,而flutter.gradle中通过buildtype来确定flutter的buildmode,在Android Gradle Plugin 3.+版本中,这个buildtype的问题已经得到解决,这也可能是flutter选用3.+版本的一个原因。

如果避免2.+的buildtype问题呢,网上是有一些获取project的buildtype配置给Library的方案,比如如何让library的buildType类型跟app的buildType类型一致(自由定义library的buildType) ??。 我的实现方案是摈弃通过buildtype确定flutter的buildmode的方案,通过直接读取本地local.properties中的参数来决定,这样需要自己在本地手动的进行mode的切换,尤其是要注意上线的时候修改为Release模式。不过debug模式下页面有明显的debug标识,所以一般也不会出错。将flutter.gradle 中原有的buildmodefor方法:

1
2
3
4
5
6
7
8
9
10
11
12
private static String buildModeFor(buildType) {
if (buildType.name == "profile") {
return "profile"
} else if (buildType.name == "dynamicProfile") {
return "dynamicProfile"
} else if (buildType.name == "dynamicRelease") {
return "dynamicRelease"
} else if (buildType.debuggable) {
return "debug"
}
return "release"
}

修改为:

1
2
3
private String buildModeFor(Project project) {
return resolveProperty(project, 'flutter.buildMode', 'release')
}

这样在local.properties 中就可以进行debug和Release的切换:

flutter.buildMode=release

flutter.buildMode=debug

切换mode的崩溃问题

在第一次配置好工程或者切换mode的过程中,可能会遇到以下的崩溃问题:

Check failed: vm. Must be able to initialize the VM.

主要是由于不同模式下的产物没有清理使用了缓存,解决办法是删除掉所有build文件的内容再全量编译一次就可以了。

总结

这是我在做flutter工程配置中遇到的一些坑,文风偏流水账,请大家见谅,只希望能对大家有一些借鉴意义。另外,我们已经在项目的两个模块中使用了flutter,开发效率确实能有很大提高,毕竟两端只需要一个人开发就ok,而且UI小姐姐要求的页面效果都能不折不扣的完成,上线之后目前还没发现什么问题。下一步需要做的是建立一个有效的监控体系,毕竟靠用户反馈还是不可靠也是滞后的。相信Flutter的未来一定是光明的!