即将开源 | 让Flutter真正支持View级别的混合开发
Posted 字节跳动技术团队
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了即将开源 | 让Flutter真正支持View级别的混合开发相关的知识,希望对你有一定的参考价值。
前言
Flutter发布以来,我们在今日头条主客户端多个有限场景下尝试了Flutter的落地,从实际表现上来看,整个技术栈还是不错的。上层Flutter Framework引入Widget/LayerTree等概念自己实现了界面描述框架,下层Flutter Engine把LayerTree用OpenGL渲染成用户界面,有点类似游戏引擎解决移动平台跨端开发的思路。性能方面超过目前已有的大部分跨平台动态化方案,包括RN和小程序。当然,bug还是有的,但任何处于起步阶段的新技术都存在这种情况,可以理解。
从长期来看,我们期待的是Flutter能在更多业务模块上替代Native开发,实现双端代码统一,从而减少研发成本,提升研发效率。但Native代码的迁移过程一定是渐进式的,而Flutter目前的实现机制对渐进迁移很不友好,这是目前我们在Flutter落地实践中所面临的最大问题。
问题
以android为例,我们先来看看Flutter目前的整体架构设计图(得出这张图的过程需要另起一篇Flutter整体架构分析的文章,以后再写):
几个重点:
一个FlutterView对应一个FlutterEngine实例;
一个FlutterEngine实例对应一个Dart Isolate实例;
同一个进程只有且仅有一个Dart VM虚拟机;
一个Dart VM上会存在多个Dart Isolate实例,Isolate是dart代码的执行环境;
Flutter从一开始就是为纯Flutter应用设计的,纯Flutter应用整个运行生命周期都只有一个FlutterView和Root Isolate,依靠Flutter Framework自身Route支持在FlutterView内部完成界面跳转。但Flutter+Native混合开发并非如此,正常Native Activity和Flutter Activity混合堆叠,按照目前官方的设计与实现,一定会有多个FlutterView同时存在。根据官方FAQ:
Isolates are separate heaps in Flutter’s VM
这句话意味着对于不同的FlutterView,它们背后的Isolate——也就是Dart运行环境内存完全独立,互不共享。
这就引入了一个用户数据同步的问题:FlutterView A无法获知在FlutterView B执行的数据修改操作,除非走Platform Channel这种源生开发方式把数据操作放在Native端,但这样就失去了原有的双端统一代码的优势。
这还只是开发方面对代码的限制,更为重要的是内存占用方面的问题:同一张图片可能存在N份内存缓存;再加上单个FlutterView初始内存占用就很大(毕竟背后是一整套完整的Engine/Isolate/…等对象),在低端手机上一旦FlutterView堆叠层数过多,完全有可能内存直接爆掉。
所幸我们可以基于一个在大部分情况下都成立的假设:用户同一时间只能看到一个Flutter界面。基于这个假设,我们可以做一些Hack的事情把Native界面和Flutter界面混合堆叠的场景下的内存共享和内存过多的问题规避掉。
比如我们目前的混合栈方案:
创建一个全局共享的FlutterView
打开一个FlutterActivity时把全局共享FlutterView添加到这个FlutterActivity中
FlutterActivity跳转到新页面的时候先对FlutterView截图,把截图设置为FlutterActivity背景,然后把FlutterView移除
从其他界面返回到原FlutterActivity时,再次添加FlutterView,当FlutterView首帧展示后移除截图
本质上就是用一个全局共享FlutterView规避了多FlutterView内存共享和内存过多问题,配合FlutterView和截图的添加删除实现了界面跳转返回衔接。
闲鱼之前开源的混合栈方案也是类似的原理,不再赘述。
这类方案方才也说了,是基于一个大部分情况下都成立的假设。来看看一个假设不成立的场景:我们计划要用Flutter重构实现的小视频Tab界面。
这个首页上小视频Tab共有五个频道,这五个频道并不归属同一模块,所以只能逐个频道进行Flutter重构,未重构的频道继续沿用原有Native实现。这也是渐进迁移过程中一定会存在的场景:混合页面场景——即短期内无法完成页面级别的Flutter化,Native View和多个Flutter View必须混合存在的情况。类似的还有Feed流中的Flutter Cell,列表页中的某些ItemView是FlutterView……这些场景都不满足混合栈方案的前提假设,自然方案也无法满足需求。
我们所期待的,是Flutter能支持View级别的混合开发,目前Flutter显然满足不了这个需求,难道Flutter事业就要止步于此了么?只能小打小闹的做一些独立二级页面么?
破局
在去年12月初参加Flutter Beijing Live与Google官方工程师线下沟通会时,我们就曾提及过这个难题,并期望官方能支持多FlutterView共享FlutterEngine来解决多FlutterView内存共享问题,但官方后续的Milestone中并没有涉及这个议题。于是我们只能先行进行了探索,目前代码已经初步完成,也有了Demo,正打算在小视频Tab场景中验证。
然而3月7号闲鱼发表了一篇文章《已开源|码上用它开始Flutter混合开发——FlutterBoost》,这篇文章主体是如何优化之前开源的混合栈方案,但我们更关注到了文中透漏的一个关键信息:
在混合方案方面,我们跟Google讨论了可能的一些方案。Flutter官方给出的建议是从长期来看,我们应该支持在同一个引擎支持多窗口绘制的能力,至少在逻辑上做到FlutterViewController是共享同一个引擎的资源的。换句话说,我们希望所有绘制窗口共享同一个主Isolate。
但官方给出的长期建议目前来说没有很好的支持。
这个官方的建议方案居然跟我们目前的探索方案完全一致!本来计划是过两个月等代码在线上场景验证之后再公布方案提交到官方讨论,但现在看来,既然想法一致,提前公布方案让官方review一下,给点建议让我们少走点弯路似乎更好一点。
接下来是我们完整的重构方案。
加入WindowId概念,共享Root Isolate
回到本文最开头的那张架构图,只说结论:
Flutter Framework层和Native Engine层的界面绘制交互都是通过两者的Window对象——是的,在Framework层/Engine层都叫Window。
Framework层代码参见window.dart,实例为ui.window单例对象
Engine层代码参见window.cc
两者交互的API很少,且一一对应。
#window.cc
void Window::RegisterNatives(tonic::DartLibraryNatives* natives) {
natives->Register({
{"Window_defaultRouteName", _DefaultRouteName, 1, true},
{"Window_scheduleFrame", _ScheduleFrame, 1, true},
{"Window_sendPlatformMessage", _SendPlatformMessage, 4, true},
{"Window_sendPlatformMessageSync", _SendPlatformMessageSync, 3, true},
{"Window_respondToPlatformMessage", _RespondToPlatformMessage, 3, true},
{"Window_render", _Render, 2, true},
{"Window_updateSemantics", _UpdateSemantics, 2, true},
{"Window_setIsolateDebugName", _SetIsolateDebugName, 2, true},
{"Window_addNextFrameCallback", _AddNextFrameCallback, 2, true},
{"Window_reportUnhandledException", ReportUnhandledException, 2, true},
});
}
///window.dart
class Window {
String _defaultRouteName() native 'Window_defaultRouteName';
void scheduleFrame() native 'Window_scheduleFrame';
String _sendPlatformMessage(String name,
PlatformMessageResponseCallback callback,
ByteData data) native 'Window_sendPlatformMessage';
ByteData sendPlatformMessageSync(String name, ByteData data)
native 'Window_sendPlatformMessageSync';
/// ...
}
用图来说明现有设计下多FlutterView的对应关系:
一个ioslate只有一个ui.window单例对象,只需要做一点修改:把FlutterEngine加入ID的概念传给Dart层,让dart层存在多个window,就可以实现多个FlutterEngine共享一个Ioslate了。
需要考虑的是让开发者无感知,不能让开发者显式的通过一个额外的方法去获知当前对应的ID进而获取对应的window,所以我们也修改了Engine调用dart入口main函数的代码。
/// called when isolate is already running.
@pragma("vm:entry-point")
void _runEntryPoint(Function entryPoint, String windowId, String args) {
runZoned(
() {
if (entryPoint is _UnaryFunction) {
(entryPoint as dynamic)(args != null ? json.decode(args) : null);
} else {
entryPoint();
}
},
zoneValues: {
#windowId: [windowId],
#extras: Map<dynamic, dynamic>(),
},
onError: (Object error, StackTrace stackTrace) {
_reportUnhandledException(error.toString(), stackTrace.toString());
},
);
}
利用Dart自带的神奇概念Zone,不同的Zone可以获取到不同的zoneValue这个隐藏Feature,把ID传给dart代码。
然后再改一下默认的ui.window实现。
/// The [Window] singleton. This object exposes the size of the display, the
/// core scheduler API, the input event callback, the graphics drawing API, and
/// other such core services.
/// final Window window = new Window._();
/// 以上是原实现
/// 以下是多window实现
Window get window {
String windowId = Zone.current[#windowId].first;
// must have windowId.
assert(windowId != null);
return windows[windowId];
}
以上是关于即将开源 | 让Flutter真正支持View级别的混合开发的主要内容,如果未能解决你的问题,请参考以下文章
针对特定 API 级别的自定义 Android/Flutter 版本
即将开源 | 2亿用户背后的Flutter应用框架Fish Redux