最右 JS2Flutter 框架——通信机制(三)

用户头像
刘剑
关注
发布于: 2020 年 08 月 05 日
最右JS2Flutter框架——通信机制(三)

1、概述

通信包括异步和同步两种方式,异步可根据是否关注返回结果再细分成Request-Reply和One-Way两种。JS2Flutter框架的通信机制也是在不断的迭代中逐步完善,年初发的文章Flutter动态化在最右App中的实践[1]中还是通过对JSWorkThread的阻塞,去实现Client侧到Host侧的单向同步,如今已经完成了Client和Host之间真正意义上的双向同步直连。



2、异步

2.1 One-Way

对于单向无需关注返回结果的调用,实现起来很简单,在最右JS2Flutter框架——开篇[2]中,我们讲述了Hello World实现的例子,这就是一个最简单的One-Way调用,Client和Native之间可以通过建立通信,Native和Host之间可以通过PlatformChannel建立通信,这样就完成Client和Host之间的通信。



2.2 Request-Reply

对于One-Way的调用,很容易就可以实现,假如我们需要一个返回结果呢?



举个例子,开发者想要获取剪切板数据,调用了Clipboard.getData,在运行阶段这个函数是在JS运行的,它必须向Host侧真实的Clipboard.getData发起调用,Host侧在拿到结果之后,通知Client侧,Client侧把结果给调用者。



很容易想到Client侧通过JSCore向Native注册回调函数,Native通过MethodChannel设置MethodCallHandler,当Native收到Host侧的回调时,Native调用Client侧的回调函数。JS2Flutter框架就是这样去实现的,但却又不止于此,仅仅使用回调函数来做异步的话,当遇到大量的异步回调场景时,很可能陷入"回调地狱",其实Dart为了避免类似的问题,引入了Future,JS也引入了Promise。回调函数只作为Native回调Client侧的一个句柄,真正在框架中发挥威力的是通过Future和Completer去实现的callFlutterWithReply。

Map<int, Completer> _completerMap = new Map();

Future<T> callFlutterWithReply<T>(String method, { dynamic key, String action, dynamic data }) async {
int callbackId = generateGlobalId();
invokeFlutter(method, _buildBody(key, action, data, callbackId));
Completer completer = Completer<dynamic>();
_completerMap[callbackId] = completer;
T typedResult = await completer.future;
return typedResult;
}

//当收到Native回调
...
Completer completer = _completerMap[callbackId];
if(completer != null) {
completer.complete(data);
_completerMap.remove(callbackId);
}
...



至此,Client侧面向Host侧的异步调用便建立起来了,同理,可建立Host侧面向Client侧的异步调用,这样就完成了双向的异步通信机制。



3、同步

同步的需求场景也很多,我们在Flutter动态化在最右App中的实践[1]中也有列举,比如你要通过TextPainter来测量文字的高度,是需要同步机制才能保证正确性的,当时我们是通过阻塞JSWorkThread去实现Client侧的同步,因为这是一个与UI绘制无关的线程,所以它的阻塞并不影响渲染和事件接收,但后来我们发现同步机制不仅是Client侧需要的,Host侧也有可能需要。



举个例子,我们在实现ListView控件时,如果是通过builder方式去构建,当遇到大量数据的时候,因为每一项都是实时构建,需要先询问Client侧该位置的子树并立即还原,如果是异步去刷新的话,很可能后几项先显示出来,从而造成视觉上的错乱。所以,在Host侧我们也需要建立同步机制。



那在Host侧是否可以采用同样的方案建立同步机制呢?当然也是可行的,但最右没有采用这种方案,试想一下,本来JS到Flutter就需要经历JSWorkThread、MainThread、Flutter UIThread,再来一层循环阻塞,效率肯定是大打折扣。我们采用了更高效的方式,让JS运行在Flutter的UI线程里,构建一套Client和Host之间直连的通信渠道,这样就能实现真正意义上的双向同步。



这个过程中我们需要解决两个问题:

1:如何让代码块儿能在Flutter UI线程执行?

2:怎样才能建立直连通信渠道?



要解决第一个问题,我们需要了解Flutter UI线程Work的方式,这里直接借用Gityuan的博客——Flutter渲染机制——UI线程[3],Engine是通过UITaskRunner提交任务,从而让任务在UI线程执行。理解了这一点,我们就可以扩充FlutterEngine的能力,增加一个postTaskOnUIThread函数,XCJSRuntime在启动时,让JS运行在Flutter UI线程。



第二个问题相对要复杂的多,牵涉到的面也比较广,我们先看看系统是如何建立通信渠道的,不了解的同学可以看看Gityuan的博客——深入理解Flutter的Platform Channel机制[4],理解了系统的流程(博客以Android为例,iOS侧同理)之后,首先要思考的就是我们与系统不一样的地方,Platform Channel是为了在Flutter和Platform之间建立通信渠道,它们在不同的线程,而我们是在同一个线程,所以我们不需要再去转线程。举个例子:

// |PlatformView::Delegate|
void Shell::OnPlatformViewDispatchPlatformMessage(
fml::RefPtr<PlatformMessage> message) {
FML_DCHECK(is_setup_);
FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread());

task_runners_.GetUITaskRunner()->PostTask(
[engine = engine_->GetWeakPtr(), message = std::move(message)] {
if (engine) {
engine->DispatchPlatformMessage(std::move(message));
}
});
}

// |PlatformView::Delegate|
void Shell::OnPlatformViewDispatchDirectMessage(
fml::RefPtr<DirectMessage> message) {
FML_DCHECK(is_setup_);
FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread());

engine_->DispatchDirectMessage(std::move(message));
}



有了这个基础之后,我们就可以在MethodChannel上拓展一个invokeDirectMethod函数,参照系统的实现方式,修改Dart层和Engine层,修改的路线也基本就是MethodChannel的调用流程。



这里我摘取了_DefaultBinaryMessenger中增加直连的修改,其后续的实现步骤基本上都是跟随着PlatformChannel的流程,增加直连的函数。

class _DefaultBinaryMessenger extends BinaryMessenger {
const _DefaultBinaryMessenger._();

// Handlers for incoming messages from platform plugins.
// This is static so that this class can have a const constructor.
static final Map<String, MessageHandler> _handlers =
<String, MessageHandler>{};

static final Map<String, DirectMessageHandler> _directHandlers =
<String, DirectMessageHandler>{};

// Mock handlers that intercept and respond to outgoing messages.
// This is static so that this class can have a const constructor.
static final Map<String, MessageHandler> _mockHandlers =
<String, MessageHandler>{};

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

ByteData _sendDirectMessage(String channel, ByteData message) {
return ui.window.sendDirectMessage(channel, message);
}


@override
Future<void> handlePlatformMessage(
String channel,
ByteData data,
ui.PlatformMessageResponseCallback callback,
) async {
ByteData response;
try {
final MessageHandler handler = _handlers[channel];
if (handler != null) {
response = await handler(data);
} else {
ui.channelBuffers.push(channel, data, callback);
callback = null;
}
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('during a platform message callback'),
));
} finally {
if (callback != null) {
callback(response);
}
}
}


@override
void handleDirectMessage(
String channel,
ByteData data,
ui.DirectMessageResultCallback callback,
) async {
ByteData result;
try {
final DirectMessageHandler handler = _directHandlers[channel];
if (handler != null)
result = handler(data);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('during a direct message callback'),
));
} finally {
callback(result);
}
}


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

@override
ByteData sendDirect(String channel, ByteData message) {
return _sendDirectMessage(channel, message);
}

@override
void setMessageHandler(String channel, MessageHandler handler) {
if (handler == null)
_handlers.remove(channel);
else
_handlers[channel] = handler;
ui.channelBuffers.drain(channel, (ByteData data, ui.PlatformMessageResponseCallback callback) async {
await handlePlatformMessage(channel, data, callback);
});
}

@override
void setDirectMessageHandler(String channel, DirectMessageHandler handler) {
if (handler == null)
_directHandlers.remove(channel);
else
_directHandlers[channel] = handler;
}

...
}




4、结束语

本⽂阐述了最右JS2Flutter框架的通信机制,基本上都是参照了Flutter的实现⽅式,对于同步直连的实现过程,涉及到的环节比较多,也只阐述了思想,对细节实现感兴趣的同学可追溯源码一探究竟。



5、参考文献

[1]:Flutter动态化在最右App中的实践 https://www.infoq.cn/article/MIa5AN2JE51uor4JeiPG

[2]:JS2Flutter框架——开篇 https://xie.infoq.cn/article/acee65b914dc4d0e32a5561a1

[3]:Flutter渲染机制——UI线程 http://gityuan.com/2019/06/15/flutter_ui_draw/

[4]:深入理解Flutter的Platform Channel机制 http://gityuan.com/2019/08/10/flutter_channel/



发布于: 2020 年 08 月 05 日 阅读数: 601
用户头像

刘剑

关注

还未添加个人签名 2020.06.28 加入

最右 App Android 工程师,自2019年初加入最右,主要从事 Flutter 相关领域的技术探索,负责 Flutter 的动态化在最右 App 落地及成功实践。

评论

发布
暂无评论
最右JS2Flutter框架——通信机制(三)